#include "fors_dfs_idp.h"

static
cpl_error_code calculate_mjd_end(const cpl_propertylist * plist,
		double * end);


/**
 * @brief    Create a fors_dfs_idp_property_converter
 * @param    source_key       source keyword
 * @param    dest_key    	  destination keyword
 * @param    dest_comment     destination comment (can be NULL)
 * @param    override_list    if NULL we will use the prod_list as source
 * in fors_dfs_idp_property_converter_convert, otherwise we will use
 * override_list
 * @return   newly allocated fors_dfs_idp_property_converter
 *
 * Note: the ownership of override_list is NOT transferred to the converter. source_key and
 * dest_key are copied inside the converter.
 */
fors_dfs_idp_property_converter * fors_dfs_idp_property_converter_new(const char * source_key,
		const char * dest_key, const char * dest_comment, const cpl_propertylist * override_list){

	cpl_ensure(source_key, CPL_ERROR_NULL_INPUT, NULL);
	cpl_ensure(dest_key, CPL_ERROR_NULL_INPUT, NULL);

	if(!dest_comment) dest_comment = "";

	fors_dfs_idp_property_converter * to_ret = cpl_calloc(1, sizeof(*to_ret));

	to_ret->dest_key = cpl_strdup(dest_key);
	to_ret->source_key = cpl_strdup(source_key);
	to_ret->dest_comment = cpl_strdup(dest_comment);
	to_ret->override_prop = NULL;

	if(override_list && cpl_propertylist_has(override_list, source_key)){
		const cpl_property * source_prop =
				cpl_propertylist_get_property_const(override_list, source_key);
		to_ret->override_prop =
				cpl_property_duplicate(source_prop);
	}

	return to_ret;
}

/**
 * @brief    destructor for fors_dfs_idp_property_converter
 * @param    self        the fors_dfs_idp_property_converter to destroy
 */
void fors_dfs_idp_property_converter_delete(fors_dfs_idp_property_converter *  self){

	if(!self) return;
	cpl_free(self->dest_key);
	cpl_free(self->source_key);
	cpl_free(self->dest_comment);
	cpl_property_delete(self->override_prop);
	cpl_free(self);
}

/**
 * @brief    Convert a property from product_list to append_to_list
 * @param    self        	the converter
 * @param    product_list   source list
 * @return   append_to_list destination list
 */
cpl_error_code fors_dfs_idp_property_converter_convert(const fors_dfs_idp_property_converter * self,
		const cpl_propertylist * product_list, cpl_propertylist * append_to_list){

	cpl_ensure_code(self, CPL_ERROR_NULL_INPUT);

	const cpl_property * source_property = NULL;
	if(self->override_prop){
		source_property = self->override_prop;
	}
	else{
		if(!product_list || !cpl_propertylist_has(product_list, self->source_key))
			return CPL_ERROR_DATA_NOT_FOUND;
		source_property = cpl_propertylist_get_property_const(product_list, self->source_key);
	}

	cpl_property * p = cpl_property_duplicate(source_property);

	cpl_property_set_name(p, self->dest_key);
	cpl_property_set_comment(p, self->dest_comment);
	cpl_propertylist_append_property(append_to_list, p);
	cpl_property_delete(p);
	return CPL_ERROR_NONE;
}


/**
 * @brief    Create an empty fors_dfs_idp_property_converter_list
 * @return   newly allocated fors_dfs_idp_property_converter
 *
 * Note: the ownership of override_list is NOT transferred to the converter. source_keya and
 * dest_key are copied inside the converter.
 */
fors_dfs_idp_property_converter_list *
fors_dfs_idp_property_converter_list_new(void){
	fors_dfs_idp_property_converter_list* to_ret = cpl_calloc(1, sizeof(*to_ret));

	to_ret->converters = NULL;
	to_ret->num_converters = 0;

	return to_ret;
}

/**
 * @brief    destructor for fors_dfs_idp_property_converter_list
 * @param    self        the fors_dfs_idp_property_converter to destroy
 */
void
fors_dfs_idp_property_converter_list_delete(fors_dfs_idp_property_converter_list * list){

	if(!list) return;

	for(cpl_size i = 0; i < list->num_converters; i++){
		fors_dfs_idp_property_converter_delete(list->converters[i]);
	}

	cpl_free(list->converters);
	cpl_free(list);
}

/**
 * @brief    Inserts conv inside list
 * @param    self        	the converter list
 * @param    conv 			the new converter
 * @return   error code
 *
 * Note: the ownership of conv is transferred to the converter list. An error code is
 * returned in case of error.
 */
cpl_error_code
fors_dfs_idp_property_converter_list_push_back(fors_dfs_idp_property_converter_list * self,
		fors_dfs_idp_property_converter * conv){

	cpl_ensure_code(self, CPL_ERROR_NULL_INPUT);
	cpl_ensure_code(conv, CPL_ERROR_NULL_INPUT);


	if(self->num_converters == 0){
		self->converters = cpl_calloc(1, sizeof(fors_dfs_idp_property_converter*));
	} else{
		self->converters =
				cpl_realloc(self->converters,
						sizeof(fors_dfs_idp_property_converter*) * (self->num_converters + 1));
	}

	self->converters[self->num_converters] = conv;
	self->num_converters++;

	return CPL_ERROR_NONE;
}

/**
 * @brief    Returns the number of element in the list
 * @param    self        	the converter list
 * @return   number of elements
 */
cpl_size
fors_dfs_idp_property_converter_list_get_size(const fors_dfs_idp_property_converter_list * self){
	if(!self) return 0;
	return self->num_converters;
}

/**
 * @brief    Returns the i-th element of the list
 * @param    self        	the converter list
 * @param    idx        	index of the element we want
 * @return   the i-th element, NULL in case of error
 */
const fors_dfs_idp_property_converter *
fors_dfs_idp_property_converter_list_get_const(const fors_dfs_idp_property_converter_list * self,
		const cpl_size idx){
	cpl_ensure(self, CPL_ERROR_NULL_INPUT, NULL);
	const cpl_size sz = fors_dfs_idp_property_converter_list_get_size(self);
	cpl_ensure(idx < sz && idx >= 0, CPL_ERROR_ILLEGAL_INPUT, NULL);
	return self->converters[idx];
}

/**
 * @brief    Constructor for fors_dfs_idp_converter
 * @return   a newly allocated fors_dfs_idp_converter
 */
fors_dfs_idp_converter * fors_dfs_idp_converter_new(void){
	fors_dfs_idp_converter * to_ret = cpl_calloc(1, sizeof(*to_ret));

	to_ret->converters = fors_dfs_idp_property_converter_list_new();
	to_ret->defaults_values = cpl_propertylist_new();
	return to_ret;
}

/**
 * @brief    Destructor for fors_dfs_idp_converter
 * @param    self        	the fors_dfs_idp_converter
 */
void
fors_dfs_idp_converter_delete(fors_dfs_idp_converter * self){
	if(!self) return;

	cpl_propertylist_delete(self->defaults_values);
	fors_dfs_idp_property_converter_list_delete(self->converters);
	cpl_free(self);
}

/**
 * @brief    Add a new conversion operation to fors_dfs_idp_converter
 * @param    self        	the fors_dfs_idp_converter
 * @param    source_key     see fors_dfs_idp_property_converter
 * @param    dest_key       see fors_dfs_idp_property_converter
 * @param    dest_comment   see fors_dfs_idp_property_converter
 * @param    override_list  see fors_dfs_idp_property_converter
 *
 * @return   error in case of invalid input
 */
cpl_error_code fors_dfs_idp_converter_add_conversion(
		fors_dfs_idp_converter * self,
		const char * source_key, const char * dest_key,
		const char * dest_comment,
		const cpl_propertylist * override_list){

	fors_dfs_idp_property_converter * conv = fors_dfs_idp_property_converter_new(source_key,
			dest_key, dest_comment, override_list);

	cpl_ensure_code(conv, CPL_ERROR_NULL_INPUT);

	fors_dfs_idp_property_converter_list_push_back(self->converters, conv);

	return CPL_ERROR_NONE;
}

/**
 * @brief    Add a new integer default to fors_dfs_idp_converter
 * @param    self        	the fors_dfs_idp_converter
 * @param    name     		the name of the destination key
 * @param    comment        the comment of the destination key (can be NULL)
 * @param    el       		the value
 *
 * @return   error in case of invalid input
 */
cpl_error_code fors_dfs_idp_converter_add_int_default(fors_dfs_idp_converter * self,
		const char * name, const char* comment, const int el){

	cpl_ensure_code(self, CPL_ERROR_NULL_INPUT);
	cpl_ensure_code(name, CPL_ERROR_NULL_INPUT);

	cpl_error_code fail = cpl_propertylist_append_int(self->defaults_values, name, el);
	if(fail) return fail;

	if(!comment) return CPL_ERROR_NONE;

	return cpl_propertylist_set_comment(self->defaults_values, name, comment);
}

/**
 * @brief    Add a new real default to fors_dfs_idp_converter
 * @param    self        	the fors_dfs_idp_converter
 * @param    name     		the name of the destination key
 * @param    el       		the value
 *
 * @return   error in case of invalid input
 */
cpl_error_code fors_dfs_idp_converter_add_real_default(fors_dfs_idp_converter * self,
		const char * name, const char* comment, const double el){

	cpl_ensure_code(self, CPL_ERROR_NULL_INPUT);
	cpl_ensure_code(name, CPL_ERROR_NULL_INPUT);

	cpl_error_code fail = cpl_propertylist_append_double(self->defaults_values, name, el);
	if(fail) return fail;

	if(!comment) return CPL_ERROR_NONE;

	return cpl_propertylist_set_comment(self->defaults_values, name, comment);
}

/**
 * @brief    Add a new string default to fors_dfs_idp_converter
 * @param    self        	the fors_dfs_idp_converter
 * @param    name     		the name of the destination key
 * @param    el       		the value
 *
 * @return   error in case of invalid input
 */
cpl_error_code fors_dfs_idp_converter_add_string_default(fors_dfs_idp_converter * self,
		const char * name, const char* comment,  const char * el){

	cpl_ensure_code(self, CPL_ERROR_NULL_INPUT);
	cpl_ensure_code(name, CPL_ERROR_NULL_INPUT);
	cpl_ensure_code(el, CPL_ERROR_NULL_INPUT);

	cpl_error_code fail = cpl_propertylist_append_string(self->defaults_values, name, el);
	if(fail) return fail;

	if(!comment) return CPL_ERROR_NONE;

	return cpl_propertylist_set_comment(self->defaults_values, name, comment);
}

/**
 * @brief    Add a new boolean default to fors_dfs_idp_converter
 * @param    self        	the fors_dfs_idp_converter
 * @param    name     		the name of the destination key
 * @param    el       		the value
 *
 * @return   error in case of invalid input
 */
cpl_error_code fors_dfs_idp_converter_add_boolean_default(fors_dfs_idp_converter * self,
		const char * name, const char* comment, const cpl_boolean el){

	cpl_ensure_code(self, CPL_ERROR_NULL_INPUT);
	cpl_ensure_code(name, CPL_ERROR_NULL_INPUT);

	cpl_error_code fail = cpl_propertylist_append_bool(self->defaults_values, name, el);
	if(fail) return fail;

	if(!comment) return CPL_ERROR_NONE;

	return cpl_propertylist_set_comment(self->defaults_values, name, comment);
}


/**
 * @brief    Generator of a new IDP compliant propertylist
 * @param    self        	the fors_dfs_idp_converter
 * @param    prod_plist     the product header
  *
 * @return   propertylist containing the IDP keys
 */
cpl_propertylist *
fors_dfs_idp_converter_generate_idp_propertylist(const fors_dfs_idp_converter * self,
					const cpl_propertylist * prod_plist){

	cpl_ensure(self, CPL_ERROR_NULL_INPUT, NULL);
	cpl_ensure(prod_plist, CPL_ERROR_NULL_INPUT, NULL);

	cpl_propertylist * new_list = cpl_propertylist_new();
	const fors_dfs_idp_property_converter_list * converters = self->converters;

	const cpl_size sz = fors_dfs_idp_property_converter_list_get_size(converters);
	for(cpl_size i = 0; i < sz; ++i){
		const fors_dfs_idp_property_converter * converter =
				fors_dfs_idp_property_converter_list_get_const(converters, i);
		fors_dfs_idp_property_converter_convert(converter, prod_plist, new_list);
	}

	if(self->defaults_values)
		cpl_propertylist_append(new_list, self->defaults_values);

	double mjd_end = 0;
	cpl_error_code err = calculate_mjd_end(prod_plist, &mjd_end);

	if(!err){
		cpl_propertylist_append_double(new_list, "MJD-END", mjd_end);
		cpl_propertylist_set_comment(new_list, "MJD-END", "End of observations (days)");
	}
	else{
		cpl_msg_warning(cpl_func, "Failure in calculating MJD-END");
	}

	return new_list;
}

/**
 * @brief    Generator of a new fors_dfs_idp_converter for imaging
 * @param    bias_plist     the master bias header (can NOT be null)
 * @param    phot_plist     the photometry plist (can be null)
 * @param    img     		the science image
 *
 * @return   fors_dfs_idp_converter for imaging
 */
fors_dfs_idp_converter *
fors_generate_imaging_idp_converter(const cpl_propertylist * bias_plist,
		const cpl_propertylist * phot_plist,
		const fors_image * img){

	const char * cunit_val = "deg";
	const char * bunit_val = "adu";

	fors_dfs_idp_converter * to_ret = fors_dfs_idp_converter_new();

	fors_dfs_idp_converter_add_conversion(to_ret, "ESO INS FILT1 NAME","FILTER","Filter name", NULL);
	fors_dfs_idp_converter_add_conversion(to_ret, "ESO DET WIN1 DIT1", "DIT", "Integration Time", NULL);
	//fors_dfs_idp_converter_add_conversion(to_ret, "ESO DET WIN1 DIT1", "EXPTIME");
	fors_dfs_idp_converter_add_conversion(to_ret, "ESO DET WIN1 DIT1", "TEXPTIME", "Total integration time of all exposures (s)", NULL);
	fors_dfs_idp_converter_add_conversion(to_ret, "ESO OBS PROG ID", "PROG_ID","ESO programme identification", NULL);
	fors_dfs_idp_converter_add_conversion(to_ret, "ESO OBS ID", "OBID1","Observation block ID", NULL);

	if(bias_plist){
		fors_dfs_idp_converter_add_conversion(to_ret, "ESO QC RON", "DETRON", "Detector readout noise", bias_plist);
		fors_dfs_idp_converter_add_conversion(to_ret, "ESO QC RON", "EFFRON", "Median effective readout noise", bias_plist);
	}

	fors_dfs_idp_converter_add_conversion(to_ret, "RA", "FPRA", "Footprint (J2000.0)", NULL);
	fors_dfs_idp_converter_add_conversion(to_ret, "DEC", "FPDE" ,"Footprint (J2000.0)", NULL);
	fors_dfs_idp_converter_add_conversion(to_ret, "ESO PRO TECH", "OBSTECH", "Data product category", NULL);
	fors_dfs_idp_converter_add_conversion(to_ret, "ESO PRO REC1 RAW1 NAME", "PROV1", "Originating science file", NULL);
	fors_dfs_idp_converter_add_conversion(to_ret, "ESO PRO REC1 PIPE ID", "PROCSOFT", "Data reduction software/system with version no.", NULL);


	fors_dfs_idp_converter_add_boolean_default(to_ret, "M_EPOCH", "TRUE if resulting from multiple epochs", CPL_FALSE);
	fors_dfs_idp_converter_add_boolean_default(to_ret, "SINGLEXP", "TRUE if resulting from single exposure.",CPL_TRUE);
	fors_dfs_idp_converter_add_int_default(to_ret, "NCOMBINE","# of combined raw science data files", 1);
	fors_dfs_idp_converter_add_string_default(to_ret,"PRODCATG", "Data product category", "SCIENCE.IMAGE");
	fors_dfs_idp_converter_add_string_default(to_ret,"FLUXCAL", "Certifies the validity of PHOTZP", "ABSOLUTE");
	fors_dfs_idp_converter_add_string_default(to_ret,"REFERENC", "Bibliographic reference", "");
	fors_dfs_idp_converter_add_string_default(to_ret,"BUNIT", "Physical unit of array values", bunit_val);
	fors_dfs_idp_converter_add_int_default(to_ret,"WEIGHT", "Median weight",1);
	fors_dfs_idp_converter_add_string_default(to_ret,"CUNIT1","Unit of coordinate transformation", cunit_val);
	fors_dfs_idp_converter_add_string_default(to_ret,"CUNIT2","Unit of coordinate transformation", cunit_val);
	fors_dfs_idp_converter_add_real_default(to_ret,"CSYER1", "Systematic error", 5.0);
	fors_dfs_idp_converter_add_string_default(to_ret,"PHOTSYS", "Photometric system VEGA or AB", "VEGA");


	fors_dfs_idp_converter_add_real_default(to_ret, "DATAMIN", "Minimal pixel value", cpl_image_get_min(img->data));
	fors_dfs_idp_converter_add_real_default(to_ret, "DATAMAX", "Maximum pixel value", cpl_image_get_max(img->data));
	fors_dfs_idp_converter_add_boolean_default(to_ret, "APMATCHD", "TRUE if fluxes are aperture-matched", CPL_FALSE);
	fors_dfs_idp_converter_add_boolean_default(to_ret, "ISAMP", "TRUE if image represents partially sampled sky", CPL_FALSE);

	if(phot_plist){
		fors_dfs_idp_converter_add_conversion(to_ret, "ESO QC INSTRUMENT ZEROPOINT", "PHOTZP",
				"Photometric zeropoint MAG=-2.5*log(data)+PHOTZP",
				phot_plist);
		fors_dfs_idp_converter_add_conversion(to_ret, "ESO QC INSTRUMENT ZEROPOINT ERROR", "PHOTZPER",
				"Uncertainty on PHOTZP", phot_plist);
	}

	return to_ret;
}

/*calculate MJD-END as per instructions*/
static
cpl_error_code calculate_mjd_end(const cpl_propertylist * plist,
		double * end){

	*end = 0.0;

	const char * exptime_key = "ESO DET WIN1 DIT1";
	const char * mjd_key = "MJD-OBS";

	if(!plist)
		return CPL_ERROR_DATA_NOT_FOUND;

	if(!cpl_propertylist_has(plist, mjd_key))
		return CPL_ERROR_DATA_NOT_FOUND;

	if(!cpl_propertylist_has(plist, exptime_key))
		return CPL_ERROR_DATA_NOT_FOUND;

	const double exptime = cpl_propertylist_get_double(plist, exptime_key);
	const double mjd = cpl_propertylist_get_double(plist, mjd_key);

	cpl_error_code fail = cpl_error_get_code();
	if(fail){
		cpl_error_reset();
		return fail;
	}

	*end = mjd + exptime / 86400.0;
	return CPL_ERROR_NONE;
}

