/* ----------------------------------------
 * Origin  : GFI INFORMATIQUE 
 * Project : CDPP Propagation Tool 
 * ----------------------------------------
 * History 
 * Version : 01/05/2015 : SCA : V2.1
 * Version : 07/04/2016 : SCA : V2.2 (PSWS-H2020)
 *  Add comets
 * Version : 17/10/2018 : SCA : V2.2.5
 *  Redmine #6119 
 * ----------------------------------------
 */
package fr.tlse.irap.pt.client.controller.tools.timetable;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

import fr.tlse.irap.pt.client.controller.common.Formatter;
import fr.tlse.irap.pt.client.controller.common.SpaceElementCollection;
import fr.tlse.irap.pt.client.services.ServiceFactory;
import fr.tlse.irap.spaceElement.Comet;
import fr.tlse.irap.spaceElement.Planet;
import fr.tlse.irap.spaceElement.Probe;
import fr.tlse.irap.spaceElement.Sun;
import fr.tlse.irap.tool.radial.Radial;
import fr.tlse.irap.tool.radial.RadialEnd;
import fr.tlse.irap.utils.CoordConverter;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.TableView;
import javafx.scene.paint.Color;
import javafx.util.Callback;

public class TimeTableRadialController extends TimeTableController {

	protected TableColumn<RadialProperty, String> targetCol;
	protected TableColumn<RadialProperty, String> tEndCol;
	protected TableColumn<RadialProperty, String> dvMinCol;
	protected TableColumn<RadialProperty, String> dvMaxCol;
	protected TableColumn<RadialProperty, String> deltaLongCol;
	protected TableColumn<RadialProperty, String> distSunCol;
	protected TableColumn<RadialProperty, String> longExtentCol;
	protected TableColumn<RadialProperty, String> longStartCol;

	private TableView<RadialProperty> tableView;

	boolean currentIsInsideCME = false;

	private RadialTimeTable timeTable = new RadialTimeTable();
	private RadialTimeTable timeTableRef = new RadialTimeTable();
	private Radial currentModel;

	private Thread fillTimeThread;
	private boolean isCancelledThread;
	private boolean isComputed; 

	@SuppressWarnings({ "rawtypes", "unchecked" })
	public TimeTableRadialController() {
		super();
		tableView = (TableView<RadialProperty>)tableViewRoot;
		buildColumns();
			
		final TableColumn[] columns = {targetCol, tEndCol, dvMinCol, dvMaxCol, deltaLongCol, distSunCol, longStartCol, longExtentCol};
		tableView.getColumns().setAll(columns);
		
		// Disable the columns re-ordering
		tableView.getColumns().addListener(new ListChangeListener() {
			public boolean suspended;

			@Override
			public void onChanged(Change change) {
				change.next();
				if (change.wasReplaced() && !suspended) {
					this.suspended = true;
					tableView.getColumns().setAll(columns);
					this.suspended = false;
				}
			}
		});
		
		// Sets the value of the property items with the timetable model
		tableView.setItems(timeTable.getProperties());
		
//		tableView.setRowFactory(new Callback<TableView<RadialProperty>, TableRow<RadialProperty>>() {
//		    @Override
//		    public TableRow<RadialProperty> call(TableView<RadialProperty> param) {
//		        final TableRow<RadialProperty> row = new TableRow<RadialProperty>() {
//		        	@Override
//		        	protected void updateItem(RadialProperty row, boolean empty) {
//		        		super.updateItem(row, empty);
//		        		// change row style
//		        	}
//		        };
//		        return row;
//		    }
//	    });
	}

    /**
     * Update current timetable with radial model 
     */
	public void updateModel(Radial radialModel) {
		this.currentModel = radialModel;
		isComputed = false;
	}

    /**
     * Update current timetable view
     */
	public void updateView() {
		if (!isComputed) {
			fillTimeTable();
			isComputed = true;
		}
	}

	/**
	 * Clear current timetable 
	 */
	public void resetView() {
		if (currentModel != null) {
			timeTable.clearProperties();
			timeTableRef.clearProperties();
		}
	}

	/**
	 * Build table table columns
	 */
	private void buildColumns() {

		// target column
		targetCol = new TableColumn<RadialProperty, String>(timeTable.TARGET);
		targetCol.setMinWidth(WM_COL);
		targetCol.setSortable(false);
		targetCol.setResizable(true);
		// Custom how to show data for all cells in this column.
		targetCol.setCellFactory(getCustomCellFactory());
		// Specify how to populate all cells in this column.
		targetCol.setCellValueFactory(new Callback<CellDataFeatures<RadialProperty, String>, ObservableValue<String>>() {
			@Override
			public ObservableValue<String> call(CellDataFeatures<RadialProperty, String> param) {
				if (param.getValue() != null) {
					currentIsInsideCME = param.getValue().getIsInsideCME();
					return new SimpleStringProperty(param.getValue().getTarget());
				} else {
					return new SimpleStringProperty("<no name>");
				}
			}
		});

		// endTime column
		tEndCol = new TableColumn<RadialProperty, String>();
		tEndCol.setText(timeTable.TIME_END);
		tEndCol.setMinWidth(WL_COL);
		tEndCol.setSortable(false);
		tEndCol.setResizable(true);
		tEndCol.setCellFactory(getCustomCellFactory());
		tEndCol.setCellValueFactory(new Callback<CellDataFeatures<RadialProperty, String>, ObservableValue<String>>() {
			@Override
			public ObservableValue<String> call(CellDataFeatures<RadialProperty, String> param) {
				if (param.getValue() != null) {
					return new SimpleStringProperty(param.getValue().getEndTime());
				} else {
					return new SimpleStringProperty("<null>");
				}
			}
		});

		// deltaSpeedMin column
		dvMinCol = new TableColumn<RadialProperty, String>();
		dvMinCol.setText(timeTable.DELTA_VMIN);
		dvMinCol.setMinWidth(WM_COL);
		dvMinCol.setSortable(false);
		dvMinCol.setResizable(true);
		dvMinCol.setCellFactory(getCustomCellFactory());
		dvMinCol.setCellValueFactory(new Callback<CellDataFeatures<RadialProperty, String>, ObservableValue<String>>() {
			@Override
			public ObservableValue<String> call(CellDataFeatures<RadialProperty, String> param) {
				if (param.getValue() != null) {
					return new SimpleStringProperty(param.getValue().getDeltaSpeedMin());
				} else {
					return new SimpleStringProperty("<null>");
				}
			}
		});

		// deltaSpeedMax column
		dvMaxCol = new TableColumn<RadialProperty, String>();
		dvMaxCol.setText(timeTable.DELTA_VMAX);
		dvMaxCol.setMinWidth(WM_COL);
		dvMaxCol.setSortable(false);
		dvMaxCol.setResizable(true);
		dvMaxCol.setCellFactory(getCustomCellFactory());
		dvMaxCol.setCellValueFactory(new Callback<CellDataFeatures<RadialProperty, String>, ObservableValue<String>>() {
			@Override
			public ObservableValue<String> call(CellDataFeatures<RadialProperty, String> param) {
				if (param.getValue() != null) {
					return new SimpleStringProperty(param.getValue().getDeltaSpeedMax());
				} else {
					return new SimpleStringProperty("<null>");
				}
			}
		});

		// deltaLong Start/End Column
		deltaLongCol = new TableColumn<RadialProperty, String>();
		deltaLongCol.setText(timeTable.DELTA_LONG_2L);
		deltaLongCol.setMinWidth(WM_COL);
		deltaLongCol.setSortable(false);
		deltaLongCol.setResizable(true);
		deltaLongCol.setCellFactory(getCustomCellFactory());
		deltaLongCol.setCellValueFactory(new Callback<CellDataFeatures<RadialProperty, String>, ObservableValue<String>>() {
			@Override
			public ObservableValue<String> call(CellDataFeatures<RadialProperty, String> param) {
				if (param.getValue() != null) {
					return new SimpleStringProperty(param.getValue().getDeltaLong());
				} else {
					return new SimpleStringProperty("<null>");
				}
			}
		});
		
		// distanceSun
		distSunCol = new TableColumn<RadialProperty, String>();
		distSunCol.setText(timeTable.DIST_SUN);
		distSunCol.setMinWidth(WM_COL);
		distSunCol.setSortable(false);
		distSunCol.setResizable(true);
		distSunCol.setCellFactory(getCustomCellFactory());
		distSunCol.setCellValueFactory(new Callback<CellDataFeatures<RadialProperty, String>, ObservableValue<String>>() {
			@Override
			public ObservableValue<String> call(CellDataFeatures<RadialProperty, String> param) {
				if (param.getValue() != null) {
					return new SimpleStringProperty(param.getValue().getDistanceSun());
				} else {
					return new SimpleStringProperty("<null>");
				}
			}
		});

		// longExtent (CME) Column
		longExtentCol = new TableColumn<RadialProperty, String>();
		longExtentCol.setText(timeTable.CME_EXTENT);
		longExtentCol.setMinWidth(WM_COL);
		longExtentCol.setSortable(false);
		longExtentCol.setResizable(true);
		longExtentCol.setCellFactory(getCustomCellFactory());
		longExtentCol.setCellValueFactory(new Callback<CellDataFeatures<RadialProperty, String>, ObservableValue<String>>() {
			@Override
			public ObservableValue<String> call(CellDataFeatures<RadialProperty, String> param) {
				if (param.getValue() != null) {
					return new SimpleStringProperty(param.getValue().getExtentCme());
				} else {
					return new SimpleStringProperty("<null>");
				}
			}
		});

		// longStart Column
		longStartCol = new TableColumn<RadialProperty, String>();
		longStartCol.setText(timeTable.CME_LONG);
		longStartCol.setMinWidth(WM_COL);
		longStartCol.setSortable(false);
		longStartCol.setResizable(true);
		longStartCol.setCellFactory(getCustomCellFactory());
		longStartCol.setCellValueFactory(new Callback<CellDataFeatures<RadialProperty, String>, ObservableValue<String>>() {
			@Override
			public ObservableValue<String> call(CellDataFeatures<RadialProperty, String> param) {
				if (param.getValue() != null) {
					return new SimpleStringProperty(param.getValue().getLongStart());
				} else {
					return new SimpleStringProperty("<null>");
				}
			}
		});
	}

	/**
	 * Custom how to show data for all cells in this column.
	 */
	private Callback<TableColumn<RadialProperty, String>, TableCell<RadialProperty, String>> getCustomCellFactory() {
		return new Callback<TableColumn<RadialProperty, String>, TableCell<RadialProperty, String>>() {
			@Override
			public TableCell<RadialProperty, String> call(TableColumn<RadialProperty, String> param) {
				TableCell<RadialProperty, String> cell = new TableCell<RadialProperty, String>() {

					@Override
					public void updateItem(final String item, boolean empty) {
						setText(item);
						if (!empty && item != null) {
		                	if (item.startsWith("Planets") || item.startsWith("Comets") || item.startsWith("Probes") || item.startsWith("(")) {
		                		setTextFill(Color.GRAY);
			                }
		                	else if (currentIsInsideCME) {
								setTextFill(Color.RED);
		                	}
		                	else {
		                		setTextFill(Color.BLACK);
		                	}
						}
					}
				};
				return cell;
			}
		};
	}

	/**
	 * Fill timetable process launched in a dedicated thread
	 */
	private void fillTimeTable() {
		Task<Object> worker = fillTimeTableWorker();

		worker.stateProperty().addListener(new ChangeListener<Worker.State>() {
			@Override
			public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {
				if (newState == Worker.State.SUCCEEDED || newState == Worker.State.FAILED || newState == Worker.State.CANCELLED) {
					endCursor();
				}
			}
		});

		if (fillTimeThread != null && fillTimeThread.isAlive()) {
			// Cancel the current thread before launch a new thread 
			isCancelledThread = true;
		}
		fillTimeThread = new Thread(worker);
		fillTimeThread.start();
		
		waitCursor();
	}

	/**
	 * Fill timetable process launched in a dedicated thread
	 */
	private Task<Object> fillTimeTableWorker() {
		return new Task<Object>() {
			@Override
			protected Object call() throws Exception {
				synchronized (timeTable) {
					isCancelledThread = false;
					// Initialize the timetable content with a row of text units
					initTimeTable();
					
					if (SpaceElementCollection.getSpaceElementWrapper() != null) {
						log.info("Compute propagation for SUN");
						try {
							RadialEnd rEnd = ServiceFactory.getToolService().computeRadial(currentModel, new Sun());
							fillTimeTable(getRadialProperty(rEnd));
							if (isCancelledThread) {
								log.debug("Exit current interrupted thread");
								return true;
							}
					    } catch (Exception e) {
					    	log.debug("Sun has not been computed");
					    }
						log.info("Compute propagation for probes");
						fillTimeTable(new RadialProperty("Probes"));
						for (Probe probe : SpaceElementCollection.getSpaceElementWrapper().getProbes()) {
							if (probe.isDefined()) {
								try {
									RadialEnd rEnd = ServiceFactory.getToolService().computeRadial(currentModel, probe);
									fillTimeTable(getRadialProperty(rEnd));
									if (isCancelledThread) {
										log.debug("Exit current interrupted thread");
										return true;
									}
							    } catch (Exception e) {
							    	log.debug("Probe has not been computed : " + probe.getName());
							    }
							}
						}
						log.info("Compute propagation for planets");
						fillTimeTable(new RadialProperty("Planets"));
						for (Planet planet : SpaceElementCollection.getPlanets()) {
							try {
								RadialEnd rEnd = ServiceFactory.getToolService().computeRadial(currentModel, planet);
								fillTimeTable(getRadialProperty(rEnd));
								if (isCancelledThread) {
									log.debug("Exit current interrupted thread");
									return true;
								}
						    } catch (Exception e) {
						    	log.debug("Planet has not been computed : " + planet.getName());
						    }
						}
						log.info("Compute propagation for comets");
						fillTimeTable(new RadialProperty("Comets"));
						for (Comet comet : SpaceElementCollection.getComets()) {
							try {
								RadialEnd rEnd = ServiceFactory.getToolService().computeRadial(currentModel, comet);
								fillTimeTable(getRadialProperty(rEnd));
								if (isCancelledThread) {
									log.debug("Exit current interrupted thread");
									return true;
								}
						    } catch (Exception e) {
						    	log.debug("Comet has not been computed : " + comet.getName());
						    }
						}
					}
					return true;
				}
		    }
		};
	}

	private void initTimeTable() {
		RadialProperty property = new RadialProperty("");

		// Set an empty property to create a row with text unit
		property.setTarget("");
		property.setEndTime("");
		property.setDeltaSpeedMin(UNIT_TIME);
		property.setDeltaSpeedMax(UNIT_TIME);
		property.setDeltaLong(UNIT_LONG);
		property.setDistanceSun(UNIT_DIST);
		property.setExtentCme(UNIT_LONG);
		property.setLongStart(UNIT_LONG);

		// Add current property item to the timetable model
		timeTable.clearProperties();
		timeTable.getProperties().add(property);
		
		// Add current property item to the timetable model ref
		timeTableRef.clearProperties();
		timeTableRef.getProperties().add(property);
	}

	/**
	 * Get a new RadialProperty filled with a endModel
	 * @param endModel
	 * @return
	 */
	private RadialProperty getRadialProperty(RadialEnd rEnd)  {
		final RadialProperty property = new RadialProperty(rEnd.getEndSource().getLongName());

		property.setEndTime(Formatter.toSdf(rEnd.getEndTime()));
		property.setDeltaSpeedMin(Formatter.toDecimal(rEnd.getDvMin()));
		property.setDeltaSpeedMax(Formatter.toDecimal(rEnd.getDvMax()));
		property.setDeltaLong(Formatter.toDecimal(rEnd.getLongSep()));
		property.setDistanceSun(Formatter.toDecimal(CoordConverter.toAU(rEnd.getDistEnd())));
		
		property.setExtentCme(Formatter.toDecimal(currentModel.getExtentCME()));
		property.setLongStart(Formatter.toDecimal(currentModel.getLongStartT()));
		
		property.setIsInsideCME(rEnd.isImpacted());
		
		return property;
	}

	/**
	 * Add a new row in TimeTable from a RadialProperty
	 * If the current thread cannot fill TimeTable, the process is dedicated to the main JFX thread
	 * with a lock on TimeTable resource  
	 * @param property
	 * @throws Exception
	 */
	private void fillTimeTable(final RadialProperty property) throws Exception {
	    try {
	    	// Add current property item to the timetable model
	    	timeTable.getProperties().add(property);
    		timeTableRef.getProperties().add(property);
	    	
        } catch (Exception e) {
	        e.printStackTrace();
	        log.info("Try to update timetable model in the JFX thread");
		    try {
	    		// Update timetable model in the JFX thread
	    		Platform.runLater(new Runnable() {
	    			@Override public void run() {
	    				if (!isCancelledThread) {
	    					// Add current property item to the timetable model
	    					timeTable.getProperties().add(property);
	    					timeTableRef.getProperties().add(property);
	    				}
	  			  	}
	  			});
	        } catch (Exception e2) {
		        e2.printStackTrace();
		        throw e2;
	        }
	    }
	}

	/**
	 * Show all target rows 
	 */
	@Override
	protected void filterAll() {
		if (!timeTableRef.isEmpty()) {
			timeTable.clearProperties();
			for (RadialProperty property : timeTableRef.getProperties()) {
				timeTable.addProperty(property);
			}
		}
	}

	/**
	 * Show only impacted target rows 
	 */
	@Override
	protected void filterImpacted() {
		if (!timeTableRef.isEmpty()) {
			timeTable.clearProperties();
			for (RadialProperty prop : timeTableRef.getProperties()) {
				if (prop.getEndTime() == null || prop.getEndTime().isEmpty() || prop.getIsInsideCME()) {
					timeTable.addProperty(prop);
				}
			}
		}
	}

	/**
	 * Export table of arrival times to xml format (VOTABLE)
	 * @throws IOException 
	 */
	@Override
	protected File tableToXml() throws IOException {

		File fileTT = File.createTempFile("tmpRadialTable", ".xml");
		log.info("Temporary export file is " + fileTT.getPath());
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileTT), "UTF8"));
		
		// Header
		bw.write(XML_HDR);

		// Table declaration - begin
		bw.write("<RESOURCE name=\"Table of arrival times\">\n");
		bw.write("<TABLE>\n");
		bw.write("<DESCRIPTION>" + EXPORT_HDR + "\n\n" + getHeaderExport(currentModel) + "</DESCRIPTION>\n");
		// Table fields description
		bw.write("<FIELD name=\"TARGET\" datatype=\"char\" arraysize=\"10\"/>\n");
		bw.write("<FIELD name=\"t'\" datatype=\"char\" xtype=\"dateTime\" ucd=\"time.epoch\" arraysize=\"*\"/>\n");
		bw.write("<FIELD name=\"t'min(dV)\" unit=\"hrs\" datatype=\"float\" />\n");
		bw.write("<FIELD name=\"t'max(dV)\" unit=\"hrs\" datatype=\"float\" />\n");
		bw.write("<FIELD name=\"PhiEnd(t')-PhiStart(t)\" unit=\"deg\" datatype=\"float\" />\n");
		bw.write("<FIELD name=\"r(End-Sun)\" unit=\"AU\" datatype=\"float\" />\n");
		bw.write("<FIELD name=\"dPhiCME\" unit=\"°\" datatype=\"float\" />\n");
		bw.write("<FIELD name=\"PhiCME (t)\" unit=\"°\" datatype=\"float\" />\n");
		bw.write("<FIELD name=\"inside CME\" datatype=\"boolean\" />\n");

		// Table data
		bw.write("<DATA><TABLEDATA>\n");
		for (int i = 1; i < timeTable.getProperties().size(); i++) {
			RadialProperty prop = (RadialProperty) timeTable.getProperties().get(i);
			bw.write("<TR> ");
			bw.write("<TD>" + prop.getTarget() + "</TD> ");
			if (prop.getEndTime() != null) {
				bw.write("<TD>" + prop.getEndTime() + "</TD> ");
				bw.write("<TD>" + prop.getDeltaSpeedMin() + "</TD> ");
				bw.write("<TD>" + prop.getDeltaSpeedMax() + "</TD> ");
				bw.write("<TD>" + prop.getDeltaLong() + "</TD> ");
				bw.write("<TD>" + prop.getDistanceSun() + "</TD> ");
				bw.write("<TD>" + prop.getExtentCme() + "</TD> ");
				bw.write("<TD>" + prop.getLongStart() + "</TD> ");
				bw.write("<TD>" + prop.getIsInsideCME() + "</TD> ");
			}
			bw.write("</TR>\n");
		}
		bw.write("</TABLEDATA></DATA>\n");

		// Table declaration - end
		bw.write("</TABLE>\n");
		bw.write("</RESOURCE>\n");

		// Trailer
		bw.write("</VOTABLE>\n");
		bw.close();

		return fileTT;
	}

	/**
	 * Export table of arrival times format text
	 */
	@Override
	protected File tableToAscii() throws IOException {

		File fileTT = File.createTempFile("tmpRadialTable", ".txt");
		log.info("Temporary export file is " + fileTT.getPath());
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileTT), "UTF8"));
		// Header
		bw.write(EXPORT_HDR); 
		bw.newLine(); bw.newLine();
		bw.write(getHeaderExport(currentModel)); 
		bw.newLine(); bw.newLine();
		// Table fields description
		bw.write(timeTable.TARGET + "\t\t\t\t\t" + 
				timeTable.TIME_END + "\t\t" + 
				timeTable.DELTA_VMIN + " " + 
				timeTable.DELTA_VMAX + "\t" + 
				timeTable.DELTA_LONG + "  " + 
				timeTable.DIST_SUN + "\t" + 
				timeTable.CME_EXTENT + "\t" + 
				timeTable.CME_LONG + "\t" + 
				timeTable.IS_INSIDE);
		bw.newLine();
		bw.write("\t\t\t\t\t\t\t " + 
				UNIT_TIME + "\t   " + 
				UNIT_TIME + "\t       " + 
				UNIT_LONG + "\t\t" + 
				UNIT_DIST + "\t " + 
				UNIT_LONG + "\t " + 
				UNIT_LONG);
		bw.newLine();
		// Table data
		for (int i = 1; i < timeTable.getProperties().size(); i++) {
			RadialProperty prop = (RadialProperty) timeTable.getProperties().get(i);
			bw.write(prop.getTarget() + "\t");
			if (prop.getEndTime() != null) {
				if (prop.getTarget().length() < 8) {
					bw.write("\t");
				}
				if (prop.getTarget().length() < 16) {
					bw.write("\t");
				}
				if (prop.getTarget().length() < 24) {
					bw.write("\t");
				}
				bw.write(prop.getEndTime() + "\t  ");
				bw.write(prop.getDeltaSpeedMin() + " \t    ");
				bw.write(prop.getDeltaSpeedMax() + "\t\t       ");
				bw.write(prop.getDeltaLong() + "\t\t");
				bw.write(prop.getDistanceSun() + "\t ");
				bw.write(prop.getExtentCme() + "\t");
				bw.write(prop.getLongStart() + "\t  ");
				bw.write(prop.getIsInsideCME() + "");
			}
			bw.newLine();
		}
		bw.close();

		return fileTT;
	}

	private String getHeaderExport(Radial r) {
		String header = "Source=" + r.getStartElement().getLongName()
				+ ", t=" + Formatter.toSdf(r.getStartTime())
				+ " (hrs), speed=" + Formatter.toDecimal(r.getSpeedSolarWind()) 
				+ " (km/s), error=" + Formatter.toDecimal(r.getSpeedError()) + " (km/s)";
		return header;
	}
}


