#@String(label="Username") USERNAME
#@String(label="Password", style='password', persist=false) PASSWORD
#@Long(label="Dataset ID", value=1945) id
#@String(label="Table name" , value="ResultsTable") tableName
#@String (choices={"OMERO table", "CSV file"}, style="radioButtonHorizontal") fileChoice
#@Boolean(label="Delete existing Tables", value=false) isDeleteExistingTables
#@Boolean(label="Send Measurements to OMERO", value=true) isSendNewMeasurements



/* = CODE DESCRIPTION =
 * This script summarizes individual OMERO/CSV table attached to images into one single OMERO/CSV table attached to the
 * corresponding dataset
 * A new table is generated by summarizing all image tables.
 * The new tables are OMERO.parade
 * 
 * To be compatible with omero.table, the csv file at the dataset level must contain a column with image name AND a column with image ids.
 * 
 * 
 * == INPUTS ==
 *  - credentials 
 *  - dataset id 
 *  - Table name to retrieve the correct table at the image level
 *  - Choose format of file to retrieve (csv or omero.table)
 *  - choices to delete existing or send new measurements.
 * 
 * == OUTPUTS ==
 *  - Generate a table and csv file with summary results of all images attached to the OMERO dataset
 *  - 
Generate a CSV report in the Downloads folder
 * 
 * = DEPENDENCIES =
 *  - Fiji update site OMERO 5.5-5.6
 *  - simple-omero-client-5.15.0 or later : https://github.com/GReD-Clermont/simple-omero-client
 * 
 * = INSTALLATION = 
 *  Open Script and Run
 * 
 * = AUTHOR INFORMATION =
 * Code written by Rémy Dornier, EPFL - SV - PTECH - BIOP 
 * version : v2.0.1
 * 10.10.2022
 * 
 * = COPYRIGHT =
 * © All rights reserved. ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland, BioImaging And Optics Platform (BIOP), 2022
 * 
 * Licensed under the BSD-3-Clause License:
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided 
 * that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 
 *    in the documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products 
 *     derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * == HISTORY ==
 * - 2023.06.19 : Remove unnecessary imports --v1.1
 * - 2023-06-29 : Delete table with only one API call + move to simple-omero-client 5.14.0 --v1.2
 * - 2023-10-04 : Fix bug when deleting tables if there is not table to delete --v1.3
 * - 2023-10-04 : Fix bug on counting the number of positive cells in each channel --v1.3
 * - 2023-10-04 : Rename script to "DL_Image_Group_Image_Table_To_Dataset_Table_For_HeLa" --v1.3
 * - 2023-10-16 : Replace user temporary folder input by ImageJ home directory --v1.4
 * - 2023.10.27 : Add choice between OMERO table and CSV file --v1.5
 * - 2023.10.27 : Also delete CSV files --v1.5
 * - 2023.11.14 : Update script with user template --v2.0
 * - 2024.05.10 : Update logger, CSV file generation and token separtor --v2.0.1
 */



/**
 * Main. 
 * Connect to OMERO, 
 * get the specified dataset, 
 * find each table in the child images that correspond to the tableName, 
 * process images tables, build a dataset table, 
 * send the dataset table on the specified dataset and 
 * disconnect from OMERO
 * 
 */

IJ.run("Close All", "");

// Connection to server
host = "omero-server.epfl.ch"
port = 4064
Client user_client = new Client()

try{
	user_client.connect(host, port, USERNAME, PASSWORD.toCharArray())
}catch(Exception e){
	IJLoggerError(e.toString(), "\n"+getErrorStackTraceAsString(e))
	def message = "Cannot connect to "+host+". Please check your credentials"
	JOptionPane.showMessageDialog(null, message, "ERROR", JOptionPane.ERROR_MESSAGE);
	return
}

// global variables for popup messages
hasFailed = false
hasSilentlyFailed = false
message = ""
tokenSeparator = " | "
csvSeparator = ","

// global keys for the summary report
READ = "Reading image"
IMG_TAB = "Summary added to parent"
TYPE = "Type"
NAME = "Name"
ID = "Id"
DST_TAB_DEL = "Tables deleted"
DST_CSV_DEL = "CSVs deleted"
DST_CSV_NEW = "CSV added"
DST_TAB_NEW = "Table added"

if (user_client.isConnected()){
	IJLoggerInfo("OMERO","Connected to "+host)
	List<Map<String, String>> transferSummary = new ArrayList<>()

	try{
		def dataset_wpr 
		List<ImageWrapper> img_wpr_list
		try{
			// get the selected dataset
			dataset_wpr = user_client.getDataset(id)
			// get tables attached to the current dataset
			img_wpr_list = dataset_wpr.getImages(user_client)
		}catch(Exception e){
			hasFailed = true
			message = "The images cannot be read from dataset "+id
			IJLoggerError("OMERO", message)
			throw e
		}
		
		// initialize variables
		TableWrapper dataset_table_wpr = null;
		ResultsTable rt_image = new ResultsTable()
		ResultsTable rt_dataset = new ResultsTable()
	
		boolean isCSV = true
		if(fileChoice.equals("OMERO table")){
			isCSV = false
		}
		
		for(ImageWrapper img_wpr : img_wpr_list){	
			rt_image.reset()
			rt_dataset.reset()
			
			IJLoggerInfo("OMERO","**** Working on image '"+img_wpr.getName()+"' id: "+ img_wpr.getId() +" ****")
			
			Map<String, String> imgSummaryMap = new HashMap<>()
			imgSummaryMap.put(TYPE, "Image")
			imgSummaryMap.put(NAME, img_wpr.getName())
			imgSummaryMap.put(ID, String.valueOf(img_wpr.getId()))
			
			// get the attachments from the image
			if(!isCSV){
				try{
					IJLoggerInfo("OMERO","Reading tables...")
					List<TableWrapper> table_wpr_list = img_wpr.getTables(user_client)
					IJLoggerInfo("FIJI","Convert OMERO table to ImageJ ResultsTable...")
					rt_image = convertTableToResultsTable(table_wpr_list, tableName)
					imgSummaryMap.put(READ, "Done")
				}catch(Exception e){
					hasSilentlyFailed = true
					message = "Failing to read / convert OMERO.table"						
					IJLoggerError("OMERO", message, e)
					imgSummaryMap.put(READ, "Failed")
					transferSummary.add(imgSummaryMap)
					continue
				}
			}
			else{
				try{
					IJLoggerInfo("OMERO","Reading attachments...")
					List<FileAnnotationWrapper> file_List = img_wpr.getFileAnnotations(user_client).findAll{it.getFileName().endsWith(".csv")}
					IJLoggerInfo("FIJI","Convert CSV file to ImageJ ResultsTable...")
					rt_image = convertCSVToResultsTable(user_client, file_List, tableName)
					imgSummaryMap.put(READ, "Done")
				}catch(Exception e){
					hasSilentlyFailed = true
					message = "Failing to read / convert CSV file from OMERO"						
					IJLoggerError("OMERO", message, e)
					imgSummaryMap.put(READ, "Failed")
					transferSummary.add(imgSummaryMap)
					continue
				}
			}
				
			// build the dataset table
			if(rt_image.size() > 0){
				try{
					IJLoggerInfo("FIJI","Adding new entry in the dataset table...")
					rt_dataset = buildDatasetResultsTable(rt_image, rt_dataset, img_wpr, isCSV)
		
					List<Roi> rois =  new ArrayList<>(0)
					if(dataset_table_wpr == null)
						dataset_table_wpr = new TableWrapper(user_client, rt_dataset, img_wpr.getId(), rois)
					else
						dataset_table_wpr.addRows(user_client, rt_dataset , img_wpr.getId(), rois)
					imgSummaryMap.put(IMG_TAB, "Done")
				}catch(Exception e){
					hasSilentlyFailed = true
					message = "Failing to add a new line to the dataset table"						
					IJLoggerError("FIJI", message, e)
					imgSummaryMap.put(IMG_TAB, "Failed")
				}
			}
			else{
				IJLoggerWarn("OMERO","There is no tables for image "+img_wpr.getName()+", id: "+ img_wpr.getId()) 
			}
			transferSummary.add(imgSummaryMap)
		}
		
		Map<String, String> dstSummaryMap = new HashMap<>()
		dstSummaryMap.put(TYPE, "Dataset")
		dstSummaryMap.put(NAME, dataset_wpr.getName())
		dstSummaryMap.put(ID, String.valueOf(dataset_wpr.getId()))
	
		if(dataset_table_wpr && !hasSilentlyFailed){
			
			// delete all existing tables
			if(isDeleteExistingTables){
				deletePreviousRuns(user_client, dataset_wpr, tableName, dstSummaryMap)
			}
	
			if (isSendNewMeasurements){
				// sending the dataset table on OMERO
				sendDatasetTable(user_client, dataset_wpr, dataset_table_wpr, dstSummaryMap)

				// attach the corresponding csv file
				sendDatasetCSVTable(user_client, dataset_wpr, dataset_table_wpr, dstSummaryMap)
			}
		}
		transferSummary.add(dstSummaryMap)
		
		if(hasSilentlyFailed)
			message = "The script ended with some errors. Dataset table / CSV table have NOT been added."
		else 
			message = "The dataset tables have been successfully created."

	}catch(Exception e){
		IJLoggerError(e.toString(), "\n"+getErrorStackTraceAsString(e))
		if(!hasFailed){
			hasFailed = true
			message = "An error has occurred. Please look at the logs and the report to know where the processing has failed."
		}
	}finally{
		// generate CSV report
		try{
			IJLoggerInfo("CSV report", "Generate the CSV report...")
			generateCSVReport(transferSummary)
		}catch(Exception e2){
			IJLoggerError(e2.toString(), "\n"+getErrorStackTraceAsString(e2))
			hasFailed = true
			message += " An error has occurred during csv report generation."
		}finally{
			// disconnect
			user_client.disconnect()
			IJLoggerInfo("OMERO","Disconnected from "+host)
			
			// print final popup
			if(!hasFailed) {
				message += " A CSV report has been created in your 'Downloads' folder."
				if(hasSilentlyFailed){
					JOptionPane.showMessageDialog(null, message, "The end", JOptionPane.WARNING_MESSAGE);
				}else{
					JOptionPane.showMessageDialog(null, message, "The end", JOptionPane.INFORMATION_MESSAGE);
				}
			}else{
				JOptionPane.showMessageDialog(null, message, "ERROR", JOptionPane.ERROR_MESSAGE);
			}
		}
	}
}else{
	message = "Not able to connect to "+host
	IJLoggerError("OMERO", message)
	JOptionPane.showMessageDialog(null, message, "ERROR", JOptionPane.ERROR_MESSAGE);
}
return

def sendDatasetCSVTable(user_client, dataset_wpr, dataset_table_wpr, dstSummaryMap){
	try{
		IJLoggerInfo("OMERO","Sending new dataset CSV table...") 
		ResultsTable rt = convertTableToResultsTable(dataset_table_wpr)
		def temp_folder =  Prefs.getHomeDir()
		uploadResultsTable(user_client, dataset_wpr, rt, temp_folder)
		dstSummaryMap.put(DST_CSV_NEW, "Done")
	}catch(Exception e){
		hasSilentlyFailed = true
		message = "An issue occurred when trying to send dataset CSV table"						
		IJLoggerError("OMERO", message, e)
		dstSummaryMap.put(DTS_CSV_NEW, "Failed")
	}
}


def sendDatasetTable(user_client, dataset_wpr, dataset_table_wpr, dstSummaryMap){
	try{
		IJLoggerInfo("OMERO","Sending new dataset table...") 
		dataset_table_wpr.setName(dataset_wpr.getName()+"_"+tableName)
		dataset_wpr.addTable(user_client, dataset_table_wpr)
		dstSummaryMap.put(DST_TAB_NEW, "Done")
	}catch(Exception e){
		hasSilentlyFailed = true
		message = "An issue occurred when trying to send dataset table"						
		IJLoggerError("OMERO", message, e)
		dstSummaryMap.put(DST_TAB_NEW, "Failed")
	}
}


def deletePreviousRuns(user_client, dataset_wpr, tableName, dstSummaryMap){
	// delete the previous OMERO tables
	try{
		IJLoggerInfo("OMERO","Deleting existing tables...") 
		def table_to_delete = dataset_wpr.getTables(user_client).findAll{it.getName().contains(tableName)}
		if(table_to_delete != null && !table_to_delete.isEmpty()){
			user_client.deleteTables(table_to_delete)
			IJLoggerInfo("OMERO","Tables deleted !") 
			dstSummaryMap.put(DST_TAB_DEL, "Done")
		}
		else{
			IJLoggerWarn("OMERO","No tables to delete")
		}
	}catch(Exception e){
		hasSilentlyFailed = true
		message = "An issue occurred when trying to delete tables"						
		IJLoggerError("OMERO", message, e)
		dstSummaryMap.put(DST_TAB_DEL, "Failed")
	}
	
	// delete the previous CSV files
	try{
		IJLoggerInfo("OMERO","Deleting existing CSV files...") 
		def files_to_delete = dataset_wpr.getFileAnnotations(user_client).findAll{it.getFileName().endsWith(".csv") && it.getFileName().contains(tableName)}
		if(files_to_delete != null && !files_to_delete.isEmpty()){
			user_client.delete((Collection<GenericObjectWrapper<?>>)files_to_delete)
			IJLoggerInfo("OMERO","CSV files deleted !") 
			dstSummaryMap.put(DST_CSV_DEL, "Done")
		}
		else{
			IJLoggerWarn("OMERO","No CSV files to delete")
		}
	}catch(Exception e){
		hasSilentlyFailed = true
		message = "An issue occurred when trying to delete CSV files"						
		IJLoggerError("OMERO", message, e)
		dstSummaryMap.put(DST_CSV_DEL, "Failed")
	}
}



/**
 * Filter a list to tableWrappers and convert the right table to an ImageJ ResultstTable
 */
def convertTableToResultsTable(tableList, tableName){
	TableWrapper table_wpr = tableList.find{it.getName().contains(tableName)}
	return (table_wpr == null ? new ResultsTable() : convertTableToResultsTable(table_wpr))
}


/**
 * Build the image ResultTable from an OMERO table
 */
def convertTableToResultsTable(table_wpr){
	ResultsTable rt = new ResultsTable()
	
	def data = table_wpr.getData()
	def nbCol = table_wpr.getColumnCount()
	def nbRow = table_wpr.getRowCount()
	
	// build the ResultsTable
	if(nbCol > 2){ // if we have more than a label to display
		
		for(i = 0; i < nbRow; i++){
			rt.incrementCounter()
			// add the image IDs at the end of the table to be compatible with omero.parade
			rt.setValue(table_wpr.getColumnName(0), i, data[0][i].getId())  // this is very important to get the image ids and be omero.parade compatible
			for(j = 1 ; j < nbCol; j++){
				rt.setValue(table_wpr.getColumnName(j), i, data[j][i])
			}
		}
	}
	
	return rt
}


/**
 * Build the image ResultTable from a CSV file 
 */
def convertCSVToResultsTable(user_client, fileList, tableName){
	// filter files corresponding to the specified name
	FileAnnotationWrapper fileWpr = fileList.find{it.getFileName().contains(tableName)}
	ResultsTable rt_image = new ResultsTable()
	boolean failedToReadCSV = false
	def exceptionCaught
	
	if(fileWpr != null){
		def temp_folder =  Prefs.getHomeDir() + "table.csv"
		File originalFile = null
		try{
			// download the file from the omero server
			originalFile = fileWpr.getFile(user_client, temp_folder)
			if(originalFile.exists()){
				// convert the csv file to ResultsTable
				rt_image = ResultsTable.open(originalFile.getAbsolutePath())
			}
		}catch(Exception e){
			failedToReadCSV = true
			exceptionCaught = e
		}finally{
			// delete the downloaded file
			if(originalFile != null)
				originalFile.delete()
			if(failedToReadCSV)
				throw exceptionCaught
		}
	}
	return (rt_image == null ? new ResultsTable() : rt_image)
}


/**
  * upload resultsTable as csv file
  * 
  */
def uploadResultsTable(user_client, repository_wpr, rt, temp_folder){
	def previous_name = rt.getTitle()
	analysisimage_output_path = new File (temp_folder , repository_wpr.getName().replace(" ","_") + "_"+tableName+".csv" )
	rt.save(analysisimage_output_path.toString())
	rt.show(previous_name)
	boolean failedToSendCSV = false
	def exceptionCaught
	
	try{
		// Import csv on OMERO
		repository_wpr.addFile(user_client, analysisimage_output_path)
	}catch(Exception e){
		failedToSendCSV = true
		exceptionCaught = e
	} finally{
		// delete the file after upload
		analysisimage_output_path.delete()
		if(failedToSendCSV)
			throw exceptionCaught
	}
}


/**
 * Build the dataset ResultTable. You can change this method to fit yours needs
 * (more inputs, other processings...)
 * 
 * inputs
 * 		rt_image : Image ResultsTable
 * 		rt_dataset : Dataset ResultsTable
 * 		img_wpr : OMERO image
 * 
 * */
def buildDatasetResultsTable(rt_image, rt_dataset, img_wpr, isCSV){
	def shift = 0
	if(isCSV)
		shift = -2
		
	rt_dataset.incrementCounter()
	rt_dataset.setValue("Image Name", 0, img_wpr.getName())

	// compute the number of cells
	double one_channel_size = rt_image.size()/3

	rt_dataset.setValue("ROI count", 0, one_channel_size)		
	
	// count the number of positive cells for each channel.
	// cells are considered positive if their mean intensity is larger than T
	def max_intensity_list = rt_image.getColumn(4 + shift)

	long T = 100
	long posCellChProtX = 0
	for(int i = 0; i < one_channel_size; i++){
		if(max_intensity_list[i] > T)
			posCellChProtX += 1
	}

	long posCellChOrganelle = 0 								 												 								 														 												 								 										
	for(int i = 2*one_channel_size; i < 3*one_channel_size; i++){
		if(max_intensity_list[i] > T)
			posCellChOrganelle += 1
	}		

	rt_dataset.setValue("Threshold", 0, T)
	rt_dataset.setValue("PosCellProtX", 0, posCellChProtX)
	rt_dataset.setValue("PosCellOrganelle", 0, posCellChOrganelle)
	
						 												 								 														 												 								 									
	rt_dataset.updateResults()
	rt_dataset.show("dataset_table")
	
	return rt_dataset	 	
}


/**
 * Create the CSV report from all info cleecting during the processing
 */
def generateCSVReport(transferSummaryList){
	// define the header
	def headerList = [TYPE, NAME, ID, READ, IMG_TAB, DST_TAB_DEL, DST_CSV_DEL, DST_TAB_NEW, DST_CSV_NEW]
	String header = headerList.join(csvSeparator)
	String statusOverallSummary = ""
	
	// get all summaries
	transferSummaryList.each{imgSummaryMap -> 
		def statusSummaryList = []
		//loop over the parameters
		headerList.each{outputParam->
			if(imgSummaryMap.containsKey(outputParam))
				statusSummaryList.add(imgSummaryMap.get(outputParam))
			else
				statusSummaryList.add("-")
		}
		
		statusOverallSummary += statusSummaryList.join(csvSeparator) + "\n"
	}
	String content = header + "\n"+statusOverallSummary
					
	// save the report
	def name = getCurrentDateAndHour() + "_Group_tables_to_dataset"
	String path = System.getProperty("user.home") + File.separator + "Downloads"
	IJLoggerInfo("CSV report", "Saving the report as '"+name+".csv' in "+path+"....")
	writeCSVFile(path, name, content)	
	IJLoggerInfo("CSV report", "DONE!")
}


/**
 * Save a csv file in the given path, with the given name
 */
def writeCSVFile(path, name, fileContent){
	// create the file locally
    File file = new File(path.toString() + File.separator + name + ".csv");

    try (BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
        buffer.write(fileContent);
	}catch(Exception e){
		throw e
	}
}



/**
 * Logger methods
 */
def getErrorStackTraceAsString(Exception e){
    return Arrays.stream(e.getStackTrace()).map(StackTraceElement::toString).reduce("",(a, b)->a + "     at "+b+"\n");
}
def IJLoggerError(String message){
	IJ.log(getCurrentDateAndHour() + "   [ERROR]        "+message); 
}
def IJLoggerError(String title, String message){
	IJ.log(getCurrentDateAndHour() + "   [ERROR]        ["+title+"] -- "+message); 
}
def IJLoggerError(String title, String message, Exception e){
    IJLoggerError(title, message);
    IJLoggerError(e.toString(), "\n"+getErrorStackTraceAsString(e));
}
def IJLoggerError(String message, Exception e){
    IJLoggerError(message);
    IJLoggerError(e.toString(), "\n"+getErrorStackTraceAsString(e));
}
def IJLoggerWarn(String message){
	IJ.log(getCurrentDateAndHour() + "   [WARNING]    "+message); 
}
def IJLoggerWarn(String title, String message){
	IJ.log(getCurrentDateAndHour() + "   [WARNING]    ["+title+"] -- "+message); 
}
def IJLoggerInfo(String message){
	IJ.log(getCurrentDateAndHour() + "   [INFO]             "+message); 
}
def IJLoggerInfo(String title, String message){
	IJ.log(getCurrentDateAndHour() + "   [INFO]             ["+title+"] -- "+message); 
}
def getCurrentDateAndHour(){
    LocalDateTime localDateTime = LocalDateTime.now();
    LocalTime localTime = localDateTime.toLocalTime();
    LocalDate localDate = localDateTime.toLocalDate();
    return ""+localDate.getYear()+
            (localDate.getMonthValue() < 10 ? "0"+localDate.getMonthValue():localDate.getMonthValue()) +
            (localDate.getDayOfMonth() < 10 ? "0"+localDate.getDayOfMonth():localDate.getDayOfMonth())+"-"+
            (localTime.getHour() < 10 ? "0"+localTime.getHour():localTime.getHour())+"h"+
            (localTime.getMinute() < 10 ? "0"+localTime.getMinute():localTime.getMinute())+"m"+
            (localTime.getSecond() < 10 ? "0"+localTime.getSecond():localTime.getSecond());
}


/*
 * imports  
 */
import fr.igred.omero.*
import fr.igred.omero.roi.*
import fr.igred.omero.repository.*
import fr.igred.omero.annotations.*
import ij.*
import ij.gui.Roi
import ij.measure.ResultsTable
import java.nio.charset.StandardCharsets;
import javax.swing.JOptionPane;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;