/*
** MUTextSearch.c
**
** TclOO class  TextSearch_C  implementation.
**
** This is a basic class able to search text in one pdf_page only.
** It should be used by a more generic class like mupdf::TextSearch
** able to search text in a whole pdf_document.
**
** Oct2020 - ABU fecit
*/

#include <string.h>
#include <mupdf/fitz.h>
#include <mupdf/pdf.h>

#include "tcloo_util.h"
#include "MUPage.h"
#include "xfz_search_stext.h"
#include "MUTextSearch.h"

 /* 
 ** ==========================================================================
 ** Instance data
 **   basic new/destroy
 ** ==========================================================================
 */
typedef
	struct MUTextSearch_s {
		int  lastPos;   // last search position; needed for progressive searches
	} MUTextSearch;

static
MUTextSearch *MU_new( void ) {
	MUTextSearch* self = (MUTextSearch *)calloc(1,sizeof(MUTextSearch));
	if( self == NULL ) {
		return NULL;
	}
	self->lastPos = 0;
	return self;
}

static
void MU_destroy( MUTextSearch* self ) {
	if (self==NULL) return;
	free(self);
}

 /* 
 ** ==========================================================================
 ** TclOO Class constructor/destructor
 ** ==========================================================================
 */

static 
void MY_DeleteOOMetadata( ClientData metadata ) {
	if (metadata) MU_destroy((MUTextSearch*)metadata);
}

	// Metadata type holding MUTextSearch data
static 
const Tcl_ObjectMetadataType MY_MetadataType = {
	TCL_OO_METADATA_VERSION_CURRENT,
	"mupdf::TextSearch",
	MY_DeleteOOMetadata,
	NoCloneMethod
};

	// std utility for extracting metadata (i.e. object's data)
	//  from std args of tcloo-methods
static
MUTextSearch* MY_Metadata( Tcl_ObjectContext objectCtx ) {
	Tcl_Object thisObject = Tcl_ObjectContextObject(objectCtx);
	return (MUTextSearch*)Tcl_ObjectGetMetadata(thisObject, &MY_MetadataType);
}


 // CONSTRUCTOR:
 // TCL usage:
 //   mupdf::TextSearch_C new
static
TclResCode MY_Constructor( METHOD_ARGS ) {
	// no clientData
	int skip = Tcl_ObjectContextSkippedArgs(objectContext);
	Tcl_Object thisObject = Tcl_ObjectContextObject(objectContext);
	objc -= skip;
	objv += skip;
	
	if (objc != 0) {
		Tcl_WrongNumArgs(interp, skip, objv-skip, NULL);
		return TCL_ERROR;
	}
	MUTextSearch* self = MU_new();
	
	 // save data in object's metadata
	Tcl_ObjectSetMetadata(thisObject, &MY_MetadataType, (ClientData)self);
	return TCL_OK;
}

static
TclResCode MY_Destructor( METHOD_ARGS ) {
	Tcl_Object thisObject = Tcl_ObjectContextObject(objectContext);
	 // Reset metadata:  
	//   this will invoke MY_DeleteOOMetaData that in turns invokes MU_destroy ..
	Tcl_ObjectSetMetadata(thisObject, &MY_MetadataType, NULL);
	return TCL_OK;	
}

	// WARNING !!
	// These Tcl_MethodType structs must be fixed (i.e. static or in the heap)
	//  they cannot be defined on the stack, since the Tcl_NewMethod stores its address
static const Tcl_MethodType MY_ConstructorType = {
	TCL_OO_METHOD_VERSION_CURRENT,
	"CONSTRUCTOR",
	MY_Constructor,
	NULL,
	NULL
};
static const Tcl_MethodType MY_DestructorType = {
	TCL_OO_METHOD_VERSION_CURRENT,
	"DESTRUCTOR",
	MY_Destructor,
	NULL,
	NULL
};


 /* 
 ** ==========================================================================
 ** TclOO methods
 ** ==========================================================================
 */

	// Forward declarations and Tcl_MethodType definitions.
	// ...  remember to update the methods[] list in MUxx_Setup()
DEF_METHODTYPE(_pagesearch)
// ... other DEF_METHODTYPE ...


	/* auxiliary for Method__pagesearch() :
	** return a (Tcl_ListObj) list of quads (a quad is a list of 4 numbers)
	** ( the empty list is still a valid list)
	**  or NULL on error (plus a message in interp's result).
	**  SIDE EFFECT: self->lastPos is updated.
	*/
static
Tcl_Obj *mu_SearchText(Tcl_Interp* interp, MUTextSearch* self, fz_context* ctx, fz_stext_page* stext, const char *needle, int startpos, int hit_max) {
	int lastPos = 0;
	fz_quad *hit_quad = NULL;
	int hit_count=0;
	fz_var(hit_quad);
	fz_var(hit_count);
	fz_var(lastPos);
	
	fz_try(ctx) {
		hit_quad = fz_malloc(ctx,hit_max*sizeof(fz_quad));
		hit_count = xfz_search_stext_page(ctx, stext, startpos, &lastPos, needle, hit_quad, hit_max);		
	} fz_catch(ctx) {
		if( hit_quad != NULL ) {
			fz_free(ctx,hit_quad);
		}
		Tcl_SetObjResult(interp,Tcl_NewStringObj(fz_caught_message(ctx),-1));
		return NULL;
	}
	
	int i;
	Tcl_Obj *listPtr = Tcl_NewListObj(0,NULL);
	for (i=0 ; i<hit_count; ++i ) {
		fz_rect bbox = fz_rect_from_quad(hit_quad[i]);
		
		Tcl_Obj *objv[4];
		objv[0]=Tcl_NewDoubleObj(bbox.x0); 
		objv[1]=Tcl_NewDoubleObj(bbox.y0); 
		objv[2]=Tcl_NewDoubleObj(bbox.x1); 
		objv[3]=Tcl_NewDoubleObj(bbox.y1); 
		Tcl_Obj *rectListPtr = Tcl_NewListObj(4,objv);
		
		Tcl_ListObjAppendElement(interp, listPtr, rectListPtr);
	}	
	fz_free(ctx,hit_quad);
	
	// == save last position
	self->lastPos = lastPos;
	
	return listPtr;
}

	// NOTE: search is case insensitive (like Adobe Acrobat!)
	/*
	** search for a string in the MUPage _pageObj_.
	** returns a TCL_OK and a list of max _max_hits_ quads (a quad is a list of 4 numbers)
	** ( the empty list is still a valid list)
	** On error returns TCL_ERROR (plus a message in interp's result).
	**
	**  Note: this is an hidden method and a lot of arguments checks should
	**   have been performed by the caller
	**
	**  SIDE EFFECT: self->lastPos is updated.
	*/
static
TclResCode Method__pagesearch( METHOD_ARGS ) {
	int skip = Tcl_ObjectContextSkippedArgs(objectContext);
	MUTextSearch *self = MY_Metadata(objectContext);
	objc -= skip;
	objv += skip;
	
	if (objc != 4) {
		Tcl_WrongNumArgs(interp, skip, objv-skip, "_pageObj_  _search-string_ _max_hits_ _fromTop_");
		return TCL_ERROR;
	}
	
	MUPage *mupage = xTcl_GetMUPageFromObj(interp,objv[0]);
	if ( !mupage ) {
		//  error message is : "Object must be a mupdf::Page"
		//  It's not a good explanatory message, but since this method
		//  is not called by user, it's enough.
		return TCL_ERROR;
	}
	char *needle = Tcl_GetString(objv[1]);
	if ( strlen(needle) == 0 ) {
		Tcl_SetObjResult(interp,Tcl_NewStringObj("search string cannot be empty",-1));
		return TCL_ERROR;
	}
	int max_hits;
	if ( Tcl_GetIntFromObj(interp, objv[2], &max_hits) != TCL_OK ) {
		return TCL_ERROR;
	}
	if ( max_hits < 1  ) {
		Tcl_SetObjResult(interp,Tcl_NewStringObj("-max must be greater than 0",-1));
		return TCL_ERROR;    
	}
	int fromTop = 0;
	if ( Tcl_GetBooleanFromObj(interp, objv[3], &fromTop) != TCL_OK ) {
		return TCL_ERROR;
	}
	
	fz_stext_page* stext = MUPage_fz_stext_page(mupage);
	
	if ( fromTop ) {
		self->lastPos = 0;    
	} else {
		++ self->lastPos;
	}
	
	// grab the MUPage context ..
	fz_context* ctx = MUPage_fz_context(mupage);
	
	// side effect ... self->lastPos updated..
	Tcl_Obj* resList = mu_SearchText(interp, self, ctx, stext, needle, self->lastPos, max_hits);
	if ( resList == NULL ) {
		// error message is in interp's result
		return TCL_ERROR;
	}
	Tcl_SetObjResult(interp,resList);
	return TCL_OK;
}


 /* 
 ** ==========================================================================
 ** Auxiliary public functions
 ** ==========================================================================
 */


  /*
  ** Class Setup  - it MUST be called once in xxMAINxx.c
  **
  ** Warning: remember to add a dummy "oo::class create ..."" in xxMAINxx.tcl
  */
TclResCode  MUTextSearch_Setup(Tcl_Interp *interp) {
	Tcl_Class clazz = tcloox_getClass(interp, "mupdf::TextSearch_C");
	if (clazz == NULL) { return TCL_ERROR; }
	
	tcloox_registerConstructor(interp, clazz, &MY_ConstructorType);
	tcloox_registerDestructor (interp, clazz, &MY_DestructorType);
	
	const Tcl_MethodType * methods[] = {
		&MethodType__pagesearch,  // low-level method (hidden)
		NULL
	};
	
	tcloox_registerMethods(interp, clazz, methods);
	return TCL_OK;
}


MUTextSearch* xTcl_GetMUTextSearchFromObj( Tcl_Interp* interp, Tcl_Obj* objPtr ) {
	const Tcl_Object blObject = Tcl_GetObjectFromObj(interp, objPtr);
	if ( ! blObject ) {
		// error message set in interp's result
		return NULL;
	}
	ClientData clientData = Tcl_ObjectGetMetadata(blObject, &MY_MetadataType);
	if ( clientData == NULL ) {
		Tcl_SetObjResult(interp,Tcl_NewStringObj("Object must be a mupdf::TextSearch",-1));
		return NULL;
	}
	return (MUTextSearch*)clientData;
}
