/*
** MUPage_Annots.c
**
** Auxiliary functions for MUPage.c
**
** Nov2020 - ABU fecit
*/

#include <string.h>
#include "xpdf_extension.h"
#include "MUPage_Annots.h"

typedef unsigned int bitmask;

 // common attributes for all annotations
#define ATTR_UNKNOWN  0
#define ATTR_TYPE     1<<0
#define ATTR_RECT     1<<1
#define ATTR_COLOR    1<<2
#define ATTR_OPACITY  1<<3

 // specific annotation attributes
#define ATTR_XOBJECT   1<<4
#define ATTR_ROTATE    1<<5
#define ATTR_SCALE     1<<6
#define ATTR_TOXY      1<<7
#define ATTR_VERTICES  1<<8


  // NOTE: ATTR_OPACITY not working, moreover Acrobat deeply changes 
  //  the AP/N appearance and rotation is broken ... [June 2020] [..Dec 2020]
  // For these reasons OPACITY is not enabled.  
#define ENABLE_OPACITY 0
  
static
const bitmask CommonAnnotationAttributes = 
	ATTR_TYPE|ATTR_RECT|ATTR_COLOR|ATTR_OPACITY
	;

static
const bitmask ReadOnlyAnnotationAttributes =
	ATTR_TYPE|ATTR_RECT 
	;


typedef struct {
	int id;
	const char* name;
} AttribTableEntry;
static 
const AttribTableEntry AttribTable[] = {
	{ ATTR_TYPE,     "-type"},
	{ ATTR_RECT,     "-rect"},
	{ ATTR_COLOR,    "-color"},
#if ENABLE_OPACITY
	{ ATTR_OPACITY,  "-opacity"},    
#endif
	{ ATTR_VERTICES, "-vertices"},    
	{ ATTR_XOBJECT, "-xobject"},
	{ ATTR_ROTATE, "-rotate"},
	{ ATTR_SCALE, "-scale"},
	{ ATTR_TOXY, "-to"}
};

static 
const char* attrib_name_from_attrib_id( int id ) {
	int N = sizeof(AttribTable)/sizeof(AttribTableEntry);
	for( int i=0 ; i < N ; ++i ) {
		if (id == AttribTable[i].id) return AttribTable[i].name;
	}
	return "";
}
static 
int attrib_id_from_attrib_name( const char* name ) {
	int N = sizeof(AttribTable)/sizeof(AttribTableEntry);
	for( int i=0 ; i < N ; ++i ) {
		if (strcmp(name,AttribTable[i].name)==0) return AttribTable[i].id;
	}
	return ATTR_UNKNOWN;
}


 // NOTE: muPDF provides functions like
 //  pdf_string_from_annot_type() and pdf_annot_type_from_string()
 //  for converting annotation-types from id to string and viceversa
 //  but strings are camelCase.
 //  Tcl-ish conventions require lowercase strings,
 // therefore here is my alternative ..
 
typedef struct {
	int   subtype;
	const char* subtypeStr;
	int isImplemented;
	bitmask attribs;
} annotTypeEntry;

static
const annotTypeEntry annotTypeTable[] = {
	{ PDF_ANNOT_HIGHLIGHT, "highlight" , 1, ATTR_VERTICES},
	{ PDF_ANNOT_UNDERLINE, "underline" , 1, ATTR_VERTICES},
	{ PDF_ANNOT_STRIKE_OUT,"strikeout" , 1, ATTR_VERTICES},
	{ PDF_ANNOT_SQUIGGLY,  "squiggly"  , 1, ATTR_VERTICES},
	
	{ PDF_ANNOT_STAMP, "stamp", 1, ATTR_XOBJECT|ATTR_ROTATE|ATTR_SCALE|ATTR_TOXY },
	
//  ..currently not implemented ..
	{ PDF_ANNOT_TEXT, "text" },
	{ PDF_ANNOT_LINK, "link" },
	{ PDF_ANNOT_FREE_TEXT, "freetext" },
	{ PDF_ANNOT_LINE, "line" },
	{ PDF_ANNOT_SQUARE, "square" },
	{ PDF_ANNOT_CIRCLE, "circle" },
	{ PDF_ANNOT_POLYGON, "polygon" },
	{ PDF_ANNOT_POLY_LINE, "polyline" },
	
	{ PDF_ANNOT_CARET, "caret" },
	{ PDF_ANNOT_INK, "ink" },
	{ PDF_ANNOT_POPUP, "popup" },
	{ PDF_ANNOT_FILE_ATTACHMENT, "fileattachment" },
	{ PDF_ANNOT_SOUND, "sound" },
	{ PDF_ANNOT_MOVIE, "movie" },
	{ PDF_ANNOT_WIDGET, "widget" },
	{ PDF_ANNOT_SCREEN, "screen" },
	{ PDF_ANNOT_PRINTER_MARK, "printermark" },
	{ PDF_ANNOT_TRAP_NET, "trapnet" },
	{ PDF_ANNOT_WATERMARK, "watermark" },
	{ PDF_ANNOT_3D, "3D" },
	
	{ PDF_ANNOT_UNKNOWN, "unknown" }
};

static 
const char* string_from_annot_type( int subtype ) {
	int N = sizeof(annotTypeTable)/sizeof(annotTypeEntry);
	for( int i=0 ; i < N ; ++i ) {
		if (subtype == annotTypeTable[i].subtype) return annotTypeTable[i].subtypeStr;
	}
	return "" ;  // or NULL ??
}
static 
int annot_type_from_string( const char* str ) {
	int N = sizeof(annotTypeTable)/sizeof(annotTypeEntry);
	for( int i=0 ; i < N ; ++i ) {
		if (strcmp(str, annotTypeTable[i].subtypeStr)==0) return annotTypeTable[i].subtype;
	}
	return PDF_ANNOT_UNKNOWN;
}

static 
int is_valid_attrib_for_annot_type( int attribID, int subtype ) {
	if ( attribID & CommonAnnotationAttributes ) { return 1; }
	
	int N = sizeof(annotTypeTable)/sizeof(annotTypeEntry);
	for( int i=0 ; i < N ; ++i ) {
		if (subtype ==  annotTypeTable[i].subtype) {
			return attribID & annotTypeTable[i].attribs;
		}
	}
	return 0;
}


static 
bitmask valid_attribs_for_annot_type( int subtype ) {
	int N = sizeof(annotTypeTable)/sizeof(annotTypeEntry);
	for( int i=0 ; i < N ; ++i ) {
		if (subtype ==  annotTypeTable[i].subtype)  {
			return annotTypeTable[i].attribs | CommonAnnotationAttributes;
		}
	}
	return 0x0;
}

static 
int isImplemented_annot_type( int subtype ) {
	int N = sizeof(annotTypeTable)/sizeof(annotTypeEntry);
	for( int i=0 ; i < N ; ++i ) {
		if (subtype == annotTypeTable[i].subtype) return annotTypeTable[i].isImplemented;
	}
	return 0;
}

 // return a new Tcl_Obj (string) with the list of implemented annotations
static
Tcl_Obj* xTcl_New_ImplementedAnnotations_Obj(void) {
	Tcl_Obj* resObj = Tcl_NewStringObj("",-1);   
	if ( !resObj ) return NULL;
	
	int N = sizeof(annotTypeTable)/sizeof(annotTypeEntry);
	int firstDone = 0;
	for( int i=0 ; i < N ; ++i ) {
		if (annotTypeTable[i].isImplemented) {
			const char *sep = "";
			if (!firstDone) {
				firstDone = 1;
			} else {
				sep = " ";
			}
			Tcl_AppendStringsToObj(resObj, sep , annotTypeTable[i].subtypeStr, NULL);        
		}
	}
	return resObj;
}


 // return a new Tcl_Obj (string) with the list of valid attributes for annotType 
static
Tcl_Obj* xTcl_New_ValidAttributes_Obj(int type) {
	Tcl_Obj* resObj = Tcl_NewStringObj("",-1);   
	if ( !resObj ) return NULL;
	
	bitmask validAttribs = valid_attribs_for_annot_type(type);
	int N = sizeof(AttribTable)/sizeof(AttribTableEntry);
	int firstDone = 0;
	for( int i=0 ; i < N ; ++i ) {
		if (AttribTable[i].id & validAttribs) {
			const char *sep = "";
			if (!firstDone) {
				firstDone = 1;
			} else {
				sep = " ";
			}
			Tcl_AppendStringsToObj(resObj, sep , AttribTable[i].name, NULL);
		}
	}
	return resObj;
}

  // return the center of annot's rect,
  // ASSERT: annot's rect exist!
static
fz_point annot_center(fz_context *ctx, pdf_annot *annot) {
	return xfz_rect_center( pdf_annot_rect(ctx, annot) );
}

  // return the current zoom of an annotation stamp
  // as the ratio between the annot->ap bbox and annot->bbox
  // if annot->ap does not exist, return 1.0
  // ASSERT: Annotation stamps are always scaled maintaining the proportions
static
double annot_stamp_zoom(fz_context *ctx, pdf_annot *annot) {
	pdf_obj* annot_obj = pdf_annot_obj(ctx,annot);
	pdf_obj* ap_n = pdf_dict_getl(ctx, annot_obj, PDF_NAME(AP), PDF_NAME(N), NULL);
	if (ap_n == NULL) { return 1.0; }
	 // if /Rotate key does not exist, return 0 degrees  (OK)
	int rotation = pdf_dict_get_int(ctx, annot_obj, PDF_NAME(Rotate));
	
	 // get the /Rect of the /AP/N  and then rotate it,
	 // then compare its size with the annot's rect
	fz_rect baseRect = pdf_dict_get_rect(ctx, ap_n, PDF_NAME(BBox));
	baseRect = fz_transform_rect(baseRect,fz_rotate(rotation));
	
	fz_rect rect = pdf_annot_rect(ctx, annot);
	
	 // ASSERT: a zoomed stamp always maintains the proportions
	 //  -->  therefore it's enought the ratio of the x sides    
	double sx = (rect.x1-rect.x0)/(baseRect.x1-baseRect.x0);
	return sx;
}

pdf_annot* mu_GetAnnotFromObj(Tcl_Interp*interp, Tcl_Obj* Obj, fz_context* ctx, pdf_page* page) { 
	int annotID;
	if( Tcl_GetIntFromObj(interp,Obj,&annotID) == TCL_ERROR ) {
		Tcl_SetObjResult(interp, Tcl_ObjPrintf("invalid annotID \"%s\"",Tcl_GetString(Obj)));
		return NULL;
	}
	
	pdf_annot *annot = get_annot_from_xref(ctx, page, annotID);
	if ( !annot ) {
		Tcl_SetObjResult(interp, Tcl_ObjPrintf("invalid annotID \"%d\"",annotID));
	}
	return annot;
}

  // callback for searching a matching pdf_obj
static
int xGraftedPage_MatchXObj(const xGraftedPage* rec, const void* _xobj) {
	pdf_obj* xobj = (pdf_obj*)_xobj;
	return ( pdf_objcmp(rec->ctx, rec->xobj, xobj)==0 ? 1 : 0 );
}

static
Tcl_Obj* mu_annot_get_attrib(Tcl_Interp* interp, fz_context* ctx, CollectionOf_xGraftedPages *graftedPages, pdf_annot* annot, const char* attribName) {
	int annotType = pdf_annot_type(ctx,annot);
	const char* annotTypeStr = string_from_annot_type( annotType );
	int attribID = attrib_id_from_attrib_name(attribName);
	bitmask validAttribs = valid_attribs_for_annot_type(annotType);
	
	// check if attribID is CommonAnnotationAttributes or a validAttrib
	if ( 
		(attribID & CommonAnnotationAttributes) == 0
		&& 
		(attribID & validAttribs) == 0
		) {    
		
		Tcl_Obj* resObj = Tcl_ObjPrintf("unrecognized attribute \"%s\" for a \"%s\" annotation. Valid attributes are:", attribName, annotTypeStr);
		Tcl_Obj* attrStrObj = xTcl_New_ValidAttributes_Obj(pdf_annot_type(ctx,annot));
		Tcl_AppendObjToObj(resObj, attrStrObj);
		Tcl_DecrRefCount(attrStrObj);            
		Tcl_SetObjResult(interp, resObj);
		return NULL;
	}
	
	Tcl_Obj* newObj = NULL;
	
	fz_try(ctx) {
		switch (attribID) {
			case ATTR_TYPE: {
				newObj = Tcl_NewStringObj(annotTypeStr,-1);
			}
			break;
			case ATTR_RECT: {
				fz_rect rect = pdf_annot_rect(ctx, annot);
				newObj = Tcl_NewListObj(0,NULL);
				Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)rect.x0));
				Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)rect.y0));
				Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)rect.x1));
				Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)rect.y1));                     
			}
			break;
			case ATTR_COLOR: {
				int nChannels;
				float color[4];
				char tkcolor[8+10]; // better to stay large
				pdf_annot_color(ctx, annot, &nChannels, color);
				// convert it to a tk-color string "#RRGGBB"
				PDFColorToTkColor( nChannels, color, tkcolor);
				newObj = Tcl_NewStringObj(tkcolor,-1);
			}
			break;
#if ENABLE_OPACITY
			case ATTR_OPACITY: {
			// what happens if it's not set ??  return 0 ?  .. it should ret 1.0 !!!
				newObj = Tcl_NewDoubleObj(pdf_annot_opacity(ctx, annot));
			}
			break;
#endif
			case ATTR_VERTICES: {
				// note: for each 'box' PDF requires quads (i.e 4 points)
				//   but here we only extract two extreme points.
				newObj = Tcl_NewListObj(0,NULL);
				int nquads = pdf_annot_quad_point_count(ctx, annot);
				for( int i=0 ; i< nquads ; ++i) {
					fz_quad quad = pdf_annot_quad_point(ctx, annot, i);
					Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)quad.ul.x));
					Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)quad.ul.y));
					Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)quad.lr.x));
					Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)quad.lr.y));         
				}
			}
			break;
			case ATTR_XOBJECT: {
				// return the grabbedID (as a TCL_String) matching the appearance...XObject
				//  or NULL if not matching
				newObj = NULL;
				
				pdf_obj* xdictobj = pdf_dict_getl(ctx, pdf_annot_obj(ctx,annot), PDF_NAME(AP), PDF_NAME(N), PDF_NAME(Resources), PDF_NAME(XObject), NULL);
				if ( ! xdictobj ) {
					break;
				}
				pdf_obj* xobj = pdf_dict_get_val(ctx, xdictobj, 0);  //first entry
				if ( ! xdictobj ) {
					break;
				}
				CollectionCursor graftCursor;
				if ( CollectionOf_xGraftedPages_Match(graftedPages, xGraftedPage_MatchXObj, xobj, &graftCursor) ) {
					xGraftedPage* gPage = CollectionOf_xGraftedPages_Value(&graftCursor);
					newObj = Tcl_NewStringObj(gPage->ID,-1);
				}
			}
			break;
			case ATTR_ROTATE: {
				int rot = pdf_dict_get_int(ctx,pdf_annot_obj(ctx,annot),PDF_NAME(Rotate));
				newObj = Tcl_NewIntObj(rot);
			}
			break;
			case ATTR_SCALE: {
				double zoom =  annot_stamp_zoom(ctx, annot);
				newObj = Tcl_NewDoubleObj(zoom);
			}
			break;
			case ATTR_TOXY: {
				fz_point C = annot_center(ctx,annot);
				newObj = Tcl_NewListObj(0,NULL);
				Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)C.x));
				Tcl_ListObjAppendElement(0, newObj, Tcl_NewDoubleObj((double)C.y));
			}
			break;
		}
	} fz_catch(ctx) {
		Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
		return NULL;
	}
	return newObj;
}



Tcl_Obj* mu_annot_get_attribs(Tcl_Interp* interp, fz_context* ctx, CollectionOf_xGraftedPages *graftedPages, pdf_annot* annot, int objc, Tcl_Obj* const objv[]) {
	switch (objc) {
		case 0: {
		// list all attribs
			bitmask validAttribs = valid_attribs_for_annot_type(pdf_annot_type(ctx,annot));
			int N = sizeof(AttribTable)/sizeof(AttribTableEntry);
			
			Tcl_Obj* listObj = Tcl_NewListObj(0,NULL);
			
			for( int i=0 ; i < N ; ++i ) {
				if (AttribTable[i].id & validAttribs) {
					Tcl_Obj* result =  mu_annot_get_attrib(interp, ctx, graftedPages, annot, AttribTable[i].name);
					if ( result ) {
						Tcl_ListObjAppendElement(interp, listObj, Tcl_NewStringObj(AttribTable[i].name,-1));
						Tcl_ListObjAppendElement(interp, listObj, result);
					} else {
						// ?? error ??
					}
				}
			}
			return listObj;
		}
		break;
		case 1: {
			Tcl_Obj* result;
			result =  mu_annot_get_attrib(interp, ctx, graftedPages, annot, Tcl_GetString(objv[0]));
			return result;
		}
		break;
		default:
			Tcl_SetObjResult(interp,Tcl_NewStringObj("wrong # args: should be \"annot ?get? -attrib\"",-1));
			return NULL;
	}
}



static
int xGraftedPage_MatchID(const xGraftedPage* rec, const void* _ID) {
	const char *ID = _ID;
	return ( strcmp(rec->ID,ID)==0 ? 1 : 0 );
}


  // ASSERT: checks about the validity of the options, already performed 
static
TclResCode mu_annot_set_attrib(Tcl_Interp* interp, fz_context* ctx, CollectionOf_xGraftedPages* graftedPages, pdf_annot* annot, const char* attribName, Tcl_Obj* attribValObj) {
	int attribID = attrib_id_from_attrib_name(attribName);
	
	TclResCode res = TCL_OK;
	pdf_obj* annot_obj = pdf_annot_obj(ctx,annot);
	
	fz_var(res);

	fz_try(ctx) {
		switch (attribID) {
			case ATTR_COLOR: {
				char* tkcolor =Tcl_GetString(attribValObj);
				float rgb[3];
				
				if (TkColorToPDFColor( tkcolor, rgb ) != 0) {
					Tcl_SetObjResult(interp, Tcl_ObjPrintf("unrecognized color \"%s\"", tkcolor));
					res = TCL_ERROR;
				} else {
					pdf_set_annot_color(ctx, annot, 3, rgb);
				}
			}
			break;
#if ENABLE_OPACITY
// WARNING: this is currently disabled since it breaks the AP/N and it's not
// compliant with Acrobat way of working.  [june-2020]
			case ATTR_OPACITY: {
				double opacity;
				res = Tcl_GetDoubleFromObj(interp, attribValObj, &opacity);
				if ( TCL_OK != res ) {
					; // msg already set in interp's result
				} else {
					pdf_set_annot_opacity(ctx,annot,opacity);
				}
			}
			break;
#endif
			case ATTR_VERTICES: {
				// NOTE: we pass 2 points for each 'box' and then 
				//  build a quad-box (4 points) for PDF
				Tcl_Size nCoords;
				res = Tcl_ListObjLength(interp, attribValObj, &nCoords);
				if ( TCL_OK != res ) {
					; // msg already set in interp's result
				} else {
					if (nCoords == 0 || nCoords % 4 != 0) {
						res = TCL_ERROR;
						Tcl_SetObjResult(interp, Tcl_NewStringObj("required 4x coordinates",-1));
					} else {
						float* coord = fz_malloc(ctx,(int)nCoords*sizeof(float));
						if ( xTcl_FillArrayOfFloatFromObj(interp, attribValObj, coord, (int)nCoords) < 0 ) {
							res = TCL_ERROR;
							; // msg already set in interp's result
						} else {
							res = TCL_OK;
							pdf_clear_annot_quad_points(ctx, annot);
							fz_quad quad;
							for (int i = 0; i<nCoords; i += 4 ) {
								quad.ul.x = coord[i];
								quad.ul.y = coord[i+1];
								quad.ur.x = coord[i+2];
								quad.ur.y = coord[i+1];
								
								quad.ll.x = coord[i];
								quad.ll.y = coord[i+3];
								quad.lr.x = coord[i+2];
								quad.lr.y = coord[i+3];
								
								pdf_add_annot_quad_point(ctx, annot, quad);
							}
						}
						fz_free(ctx,coord);
					}
				}
			}
			break;
			
			case ATTR_XOBJECT: {
				const char *graftID = Tcl_GetString(attribValObj);
				
				CollectionCursor graftCursor;
				if (! CollectionOf_xGraftedPages_Match(graftedPages,xGraftedPage_MatchID, graftID, &graftCursor)) {
					Tcl_SetObjResult(interp, Tcl_ObjPrintf("\"%s\" is not a saved graft", graftID));
					res = TCL_ERROR;
				} else {
					xGraftedPage* gPage = CollectionOf_xGraftedPages_Value(&graftCursor);
					
					// get the annot's zoom BEFORE replacing the annot appearance
					double zoom =  annot_stamp_zoom(ctx, annot);
					int rotation = pdf_dict_get_int(ctx, annot_obj, PDF_NAME(Rotate));                     
					
					fz_rect baseRect = pdf_dict_get_rect(ctx, gPage->xobj, PDF_NAME(BBox));
					
					// add or replace annot->ap
					pdf_obj* ap = pdf_dict_put_dict(ctx,annot_obj,PDF_NAME(AP),1);
					pdf_obj *ap_xobj = xpdf_xobject_new(ctx, pdf_annot_page(ctx,annot)->doc, gPage->xobj, fz_identity, baseRect );
					
					pdf_dict_put(ctx,ap,PDF_NAME(N), ap_xobj );
					
					// create a /Name.
					// For Acrobat compatibility, it must start with "#"
					//  or the appearance will be broken when rotated.
					// see https://stackoverflow.com/questions/43052760/itext-7-generated-stamp-breaks-upon-rotation
					pdf_dict_put_name(ctx,annot_obj,PDF_NAME(Name),"#abu9283");
					// the name is not important,nor its uniqueness. it must start with #
					
					fz_point OP = xfz_rect_center(baseRect);
					fz_rect stamp_rect = pdf_annot_rect(ctx, annot);
					fz_point P;
					if ( ! fz_is_empty_rect(stamp_rect) ) {
						P = xfz_rect_center(stamp_rect);
					} else {
						// center of page
						P = xfz_rect_center(pdf_bound_page(ctx, pdf_annot_page(ctx,annot),FZ_CROP_BOX));
					}
					// starting from the baseRect,
					// apply scale and rotation ( around the central point)
					// then collimate with the center of annot's rect.
					fz_matrix m = fz_translate(-OP.x,-OP.y);
					m = fz_concat(m,fz_rotate(rotation));
					m = fz_concat(m,fz_scale(zoom,zoom));
					m = fz_concat(m,fz_translate(P.x,P.y));
					
					// update stamp rect
					stamp_rect = fz_transform_rect(baseRect,m);
					pdf_set_annot_rect(ctx, annot, stamp_rect);
					
					if (rotation != 0) {
						// update AP/N /Matrix
						pdf_obj* xobj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(AP), PDF_NAME(N), NULL);
						
						fz_matrix mtx = fz_rotate(rotation);
						pdf_dict_put_matrix(ctx,xobj,PDF_NAME(Matrix),mtx);                    
					}
				}
			}
			break;
			
			case ATTR_ROTATE: {
				int rotation;
				if ( Tcl_GetIntFromObj(interp, attribValObj, &rotation) != TCL_OK ) {
					// error already stored in interp result
					return TCL_ERROR;
				}
				pdf_obj* xobj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(AP), PDF_NAME(N), NULL);
				if (xobj==NULL) {
					// there's nothing to rotate !
					//  anyway, don't raise an error
					// store new rotation
					pdf_dict_put_int(ctx, annot_obj, PDF_NAME(Rotate), rotation);
					return TCL_OK;
				}
				
				double zoom =  annot_stamp_zoom(ctx, annot);
				fz_rect rect = pdf_annot_rect(ctx, annot);
				fz_rect baseRect = pdf_dict_get_rect(ctx, xobj, PDF_NAME(BBox));
				
				fz_point P = xfz_rect_center(rect);
				fz_point OP = xfz_rect_center(baseRect);
				
				// starting from the baseRect,
				// apply scale and rotation ( around the central point)
				// then collimate with the center of annot's rect.
				fz_matrix m = fz_translate(-OP.x,-OP.y);
				m = fz_concat(m,fz_rotate(rotation));
				m = fz_concat(m,fz_scale(zoom,zoom));
				m = fz_concat(m,fz_translate(P.x,P.y));
				rect = fz_transform_rect(baseRect,m);
				
				// update xobj /Matrix
				fz_matrix mtx = fz_rotate(rotation);
				pdf_dict_put_matrix(ctx,xobj,PDF_NAME(Matrix),mtx);
				
				// update rect (and rotation)               
				pdf_dict_put_int(ctx, annot_obj, PDF_NAME(Rotate), rotation);             
				pdf_set_annot_rect(ctx, annot, rect);
			} 
			break; 
			case ATTR_SCALE: {
				double zoom;
				if ( Tcl_GetDoubleFromObj(interp, attribValObj, &zoom) != TCL_OK ) {
					// error already stored in interp result
					return TCL_ERROR;
				}
				pdf_obj* xobj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(AP), PDF_NAME(N), NULL);
				if (xobj==NULL) {
					// there's nothing to zoom !
					//  anyway, don't raise an error
					return TCL_OK;
				}
				
				int rotation = pdf_dict_get_int(ctx, annot_obj, PDF_NAME(Rotate));            
				
				fz_rect baseRect = pdf_dict_get_rect(ctx, xobj, PDF_NAME(BBox));
				fz_point OP = xfz_rect_center(baseRect);
				fz_rect rect = pdf_annot_rect(ctx, annot);
				fz_point P = xfz_rect_center(rect);
				
				// starting from the baseRect,
				// apply scale and rotation ( around the central point)
				// then collimate with the center of annot's rect.
				fz_matrix m = fz_translate(-OP.x,-OP.y);
				m = fz_concat(m,fz_rotate(rotation));
				m = fz_concat(m,fz_scale(zoom,zoom));
				m = fz_concat(m,fz_translate(P.x,P.y));
				rect = fz_transform_rect(baseRect,m);
				
				pdf_dict_put_matrix(ctx,xobj,PDF_NAME(Matrix),m);
				// update annot's rect               
				pdf_set_annot_rect(ctx, annot, rect);
			} 
			break; 
			case ATTR_TOXY: {
				fz_point newC; // fz_point is a float [2]
				if( xTcl_Get_N_Floats_FromObj( interp, attribValObj, 2, (float*)&newC ) != TCL_OK ) {
					return TCL_ERROR;
				}
				fz_point C = annot_center(ctx,annot);
				
				// translate rect (newC-C)
				fz_rect rect = fz_transform_rect(pdf_annot_rect(ctx, annot),
				fz_translate(newC.x-C.x,newC.y-C.y));
				pdf_set_annot_rect(ctx, annot, rect);
			}
			break;
		}
	} fz_catch(ctx) {
		Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
		return TCL_ERROR;
	}
	return res;
}

pdf_annot* get_annot_from_xref( fz_context* ctx, pdf_page* page, int xref ) {
	pdf_annot *annot;
	pdf_annot* xannot = NULL;
	fz_var(xannot);
	fz_try(ctx) {
		for (annot=pdf_first_annot(ctx,page) ; annot ; annot = pdf_next_annot(ctx,annot)) {
			if ( xref == pdf_to_num(ctx,pdf_annot_obj(ctx,annot)) ) {
				xannot=annot;
				break;
			}
		}
	} fz_catch(ctx) {
		return NULL;
	}
	return xannot;
}


 // return a TCL list , each elem is an annotID  (as TCL_String)
 // on error return NULL (error message is left in interp's result)
Tcl_Obj*  mu_annots(Tcl_Interp* interp, fz_context* ctx, pdf_page* page) {
	Tcl_Obj *annotsList = Tcl_NewListObj(0,NULL);
	
	fz_var(annotsList);
	fz_try(ctx) {
		pdf_annot *annot;
		for (annot=pdf_first_annot(ctx,page) ; annot ; annot = pdf_next_annot(ctx,annot)) {
			Tcl_ListObjAppendElement(0, annotsList, Tcl_NewIntObj(pdf_to_num(ctx, pdf_annot_obj(ctx,annot))));
		}
	} fz_catch(ctx) {
		Tcl_DecrRefCount(annotsList);
		Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
		annotsList = NULL;
	}
	return annotsList;
}


  // preliminary checks on options
static
TclResCode mu_annot_options_check(Tcl_Interp* interp, int annotType, int objc, Tcl_Obj* const objv[]) {
	bitmask validAttrs = valid_attribs_for_annot_type(annotType);
	for (int i=0;  i<objc; i+=2 ) {
		char* attribStr = Tcl_GetString(objv[i]);
		int attribID = attrib_id_from_attrib_name(attribStr);
		
		if ( (attribID & ReadOnlyAnnotationAttributes) != 0x00 ) {
			Tcl_SetObjResult(interp, Tcl_ObjPrintf("\"%s\" is a read-only option.", attribStr));
			return TCL_ERROR;
		}
		if ( (attribID & validAttrs) == 0x00 ) {
			Tcl_Obj* resObj = Tcl_ObjPrintf("\"%s\" is not a valid option for annotation \"%s\". Valid options are: ", 
			attribStr, string_from_annot_type(annotType));
			Tcl_Obj* attrStrObj = xTcl_New_ValidAttributes_Obj(annotType);
			Tcl_AppendObjToObj(resObj, attrStrObj);
			Tcl_DecrRefCount(attrStrObj);
			Tcl_SetObjResult(interp, resObj);
			return TCL_ERROR;
		}
	}
	// since options/values should be paired, if (objc-1) (the last objv[]) is even,
	// it means that the option has no value.
	if ( (objc-1) %2 == 0) {
		Tcl_SetObjResult(interp, Tcl_ObjPrintf("Missing value for the last option \"%s\"", Tcl_GetString(objv[objc-1])));
		return TCL_ERROR;
	}
	return TCL_OK;
}




pdf_annot* mu_annot_create( Tcl_Interp* interp, fz_context* ctx, CollectionOf_xGraftedPages* graftedPages, pdf_page* page, const char* annotTypeStr, int objc, Tcl_Obj* const objv[]) {
	int type = annot_type_from_string(annotTypeStr);
	if ( ! isImplemented_annot_type(type) ) {
		Tcl_Obj* resObj = Tcl_ObjPrintf("unrecognized annotation type \"%s\". Valid annotations are: ", annotTypeStr);
		Tcl_Obj* attrStrObj = xTcl_New_ImplementedAnnotations_Obj();
		Tcl_AppendObjToObj(resObj, attrStrObj);
		Tcl_DecrRefCount(attrStrObj);            
		Tcl_SetObjResult(interp, resObj);
		return NULL;
	}
	
	// special checks for each type: mandatory options
	switch (type) {
	  case PDF_ANNOT_HIGHLIGHT:
	  case PDF_ANNOT_UNDERLINE:
	  case PDF_ANNOT_STRIKE_OUT:
	  case PDF_ANNOT_SQUIGGLY: {
		// is -vertices present ?
		int isPresent = 0;
		for (int i=0;  i<objc; i+=2 ) {
			char* attribStr = Tcl_GetString(objv[i]);
			int attribID = attrib_id_from_attrib_name(attribStr);
			if (attribID == ATTR_VERTICES) {
				isPresent = 1;
				break;
			}
		}
		if (!isPresent) {
			Tcl_SetObjResult(interp, Tcl_ObjPrintf("When creating a new \"%s\" annotation, option \"%s\" must be present.", annotTypeStr, attrib_name_from_attrib_id(ATTR_VERTICES)));
			return NULL;
		}
	  }
	  break;
	  case PDF_ANNOT_STAMP:
	// -xobject must be the first option
		if ( attrib_id_from_attrib_name(Tcl_GetString(objv[0])) != ATTR_XOBJECT ) {
			Tcl_SetObjResult(interp, Tcl_ObjPrintf("When creating a new \"stamp\" annotation, option \"%s\" must be the first option", attrib_name_from_attrib_id(ATTR_XOBJECT)));
			return NULL;
		}
	  break;
	}
	
	pdf_annot *annot;
	
	fz_try(ctx) {
		annot = pdf_create_annot(ctx, page, type);
		if (annot) {
			// patch for stamps ... annot_rect will be resized later
			if (type == PDF_ANNOT_STAMP) {
				pdf_set_annot_rect(ctx,annot,pdf_bound_page(ctx, pdf_annot_page(ctx,annot),FZ_CROP_BOX));
			}
			TclResCode res;
			res = mu_annot_set_attribs(interp, ctx, graftedPages, annot, objc, objv);
			if (res != TCL_OK) {
				pdf_delete_annot(ctx, page, annot);
				annot = NULL;
			}
		}
	} fz_catch(ctx) {
		Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
		return NULL;
	}
	return annot;
}

 // WARNING:
 //  when processing multiple attributes,
 //  if an attribute is wrong, processing is terminated with TCL_ERROR (ok),
 //  but all the preceeding attributes wont't be canceled (i.e. no 'rollback')
 //  ** This is an implementation LIMITATION that should be removed. **
TclResCode mu_annot_set_attribs(Tcl_Interp* interp, fz_context* ctx,  CollectionOf_xGraftedPages *graftedPages, pdf_annot* annot, int objc, Tcl_Obj* const objv[]) {
	if ( objc == 0 ) return TCL_OK;
	
	if ( mu_annot_options_check(interp, pdf_annot_type(ctx,annot), objc, objv) != TCL_OK ) 
		return TCL_ERROR;
	
	TclResCode res;
	for( int i=0 ; i<objc; i += 2 ) {
		const char* attribName = Tcl_GetString(objv[i]);    
		res = mu_annot_set_attrib(interp, ctx, graftedPages, annot, attribName, objv[i+1]);
		if ( res != TCL_OK ) { return res; }
	}
	
	fz_try(ctx) {
		// this is VERY IMPORTANT !
		// appearance of the "stamp" annots should not be changed
		if (pdf_annot_type(ctx,annot) == PDF_ANNOT_STAMP ) {
			// # DON'T UPDATE !
			// this is VERY IMPORTANT !
			// these hacked appearances should not by changed by mupdf.
			pdf_set_annot_resynthesised(ctx,annot);

		} else {
			pdf_update_annot(ctx, annot);
		}
	} fz_catch(ctx) {
		Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
		return TCL_ERROR;
	}
	
	return TCL_OK;
}

TclResCode mu_annot_delete(Tcl_Interp* interp, fz_context* ctx, pdf_page *page, int objc, Tcl_Obj* const objv[]) {
	 // before processing some annotIDs, check if they are ALL valid
	TclResCode resCode = TCL_OK;
	
	pdf_annot** annot = fz_malloc(ctx,objc*sizeof(pdf_annot*));
	for( int i=0 ; i<objc; ++i ) {
		annot[i] = mu_GetAnnotFromObj(interp, objv[i], ctx, page);
		if ( !annot[i] ) { resCode = TCL_ERROR; break; }
	}
	if ( resCode == TCL_OK ) {
		for( int i=0 ; i<objc; ++i ) {
			 // WARNING: check if annot[id] was listed before !
			int duplicated = 0;
			for (int j=0 ; j < i ; ++j) {
				if (annot[i] == annot[j] ) { duplicated = 1 ; break; }
			}
			if (duplicated) continue;
			
			fz_try(ctx) {
				 // pdf_delete_annot() internally deletes popup and other related objects.
				pdf_delete_annot(ctx, pdf_annot_page(ctx,annot[i]), annot[i]);
			} fz_catch(ctx) {
				Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
				resCode = TCL_ERROR;
			}
			if (resCode == TCL_ERROR) break;
		}
	}
	fz_free(ctx,annot);
	return resCode;
}

TclResCode mu_annot_flatten(Tcl_Interp* interp, fz_context* ctx, pdf_page *page, int objc, Tcl_Obj* const objv[]) {
	 // before processing some annotIDs, check if they are ALL valid
	TclResCode resCode = TCL_OK;
	
	pdf_annot** annot = fz_malloc(ctx,objc*sizeof(pdf_annot*));
	for( int i=0 ; i<objc; ++i ) {
		annot[i] = mu_GetAnnotFromObj(interp, objv[i], ctx, page);
		if ( !annot[i] ) { resCode = TCL_ERROR; break; }
	}
	if ( resCode == TCL_OK ) {
		for( int i=0 ; i<objc; ++i ) {
			 // WARNING: check if annot[id] was listed before !
			int duplicated = 0;
			for (int j=0 ; j < i ; ++j) {
				if (annot[i] == annot[j] ) { duplicated = 1 ; break; }
			}
			if (duplicated) continue;
			
			fz_try(ctx) {
				xpdf_annot_flatten(ctx, annot[i]);
			} fz_catch(ctx) {
				Tcl_SetObjResult(interp, Tcl_NewStringObj(fz_caught_message(ctx),-1));
				resCode = TCL_ERROR;
			}
			if (resCode == TCL_ERROR) break;
		}
	}
	fz_free(ctx,annot);
	return resCode;
}
