/**
 ** -----------------------------------------------------------------------------**
 ** LwirReader.java
 **
 ** Multithreaded reading cameras - visible and LWIR
 **
 **
 ** Copyright (C) 2019 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  LwirReader.java is free software: you can redistribute it and/or modify
 **  it under the terms of the GNU General Public License as published by
 **  the Free Software Foundation, either version 3 of the License, or
 **  (at your option) any later version.
 **
 **  This program is distributed in the hope that it will be useful,
 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 **  GNU General Public License for more details.
 **
 **  You should have received a copy of the GNU General Public License
 **  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ** -----------------------------------------------------------------------------**
 **
 */
package com.elphel.imagej.lwir;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import com.elphel.imagej.readers.ImagejJp4Tiff;
import com.elphel.imagej.readers.ImagejJp4TiffMulti;

import ij.IJ;
import ij.ImagePlus;
import ij.Prefs;
import ij.io.FileSaver;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;
import loci.formats.FormatException;

public class LwirReader {
	public static String [] BASE_URLS= {
			"http://192.168.0.36:2323/bchn0",
			"http://192.168.0.36:2324/bchn0",
			"http://192.168.0.36:2325/bchn0",
			"http://192.168.0.36:2326/bchn0",
			"http://192.168.0.38:2323/bchn4",
			"http://192.168.0.38:2324/bchn4",
			"http://192.168.0.38:2325/bchn4",
			"http://192.168.0.38:2326/bchn4",
			};

	public static final int [] IMGSRV_PORTS = {2323,2324,2325,2326};
	public static final String IMAGE_URL_FIRST="/towp/wait/img/save";     // next image name, same image number/time
	public static final String IMAGE_URL_NEXT= "/torp/next/wait/img/save"; // same image name, same image number/time
	public static final String SKIP_FRAME_URL= "/towp/wait/meta";          // Wit for the next frame, return meta data
	public static final int COLOR_JP4 = 5;
	public static final int COLOR_RAW = 15;
	public static final int LWIR_HEIGHT = 120;
	public static final int LWIR_TELEMETRY_LINES = 2;

	public static final int FRAMES_SKIP = 4;
	public static final int MAX_THREADS = 100; // combine from all classes?

	public static final int FRAMES_AHEAD = 5;

	/** Logger for this class. */
	private static final Logger LOGGER =
			LoggerFactory.getLogger(LwirReader.class);

	private ImagejJp4TiffMulti imagejJp4TiffMulti;
	private boolean out_of_sync = false;

	/** Configuration parameters */
	private LwirReaderParameters lwirReaderParameters = null;
	private LwirReaderParameters last_programmed = null;


	private int [] motorsPosition = null;
	public boolean reportTiming=false;

	// -- constructors

	public LwirReader() {
		this.lwirReaderParameters = new LwirReaderParameters(LwirReaderParameters.NAME_TALON); // default
		imagejJp4TiffMulti = null;
	}

	public LwirReader(LwirReaderParameters lwirReaderParameters) {
		if (lwirReaderParameters == null) {
			this.lwirReaderParameters = new LwirReaderParameters(LwirReaderParameters.NAME_TALON); // default
		} else {
			this.lwirReaderParameters = lwirReaderParameters;
		}
		imagejJp4TiffMulti = null;
	}


   	public void setMotorsPosition (int [] position) {
   		this.motorsPosition=position;
   	}

   	public int [] getMotorsPosition() {
   		return this.motorsPosition;  // may be null
   	}


	// TODO: create
	public boolean reSyncNeeded() {
		return out_of_sync;
	}

	public ImagePlus[][] readAllMultiple(
			final int     num_frames,
//			final boolean telemetry,
			final boolean show,
			final boolean scale) {
		return readAllMultiple(num_frames, show, scale, "STD_");
//		return readAllMultiple(num_frames, telemetry, show, scale, "STD_");
	}

	public ImagePlus[][] readAllMultiple(
			final int     num_frames,
//			final boolean telemetry,
			final boolean show,
			final boolean scale,
			final String  std)
	{
		String [] urls0 = new String[BASE_URLS.length];
		String [] urls1 = new String[BASE_URLS.length];
		ImagePlus [][] imps = new ImagePlus[num_frames][BASE_URLS.length];
		for (int i = 0; i < BASE_URLS.length; i++) {
			urls0[i] = BASE_URLS[i] + IMAGE_URL_FIRST;
			urls1[i] = BASE_URLS[i] + IMAGE_URL_NEXT;
		}
		if (imagejJp4TiffMulti == null) {
			imagejJp4TiffMulti = new ImagejJp4TiffMulti();
			for (int i = 0; i < 2; i++) {
				try {
//					imagejJp4TiffMulti.getMultiImages(urls0, imps[0],  telemetry, scale,  std);
					imagejJp4TiffMulti.getMultiImages(urls0, imps[0],  scale,  std);
				} catch (IOException e) {
					LOGGER.error("readAllMultiple0: IOException, priming" );
				} catch (FormatException e) {
					LOGGER.error("readAllMultiple0:FormatException, priming");
				}
				LOGGER.info("priming..."+(i+1));
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		for (int n = 0; n < num_frames; n++) {
			LOGGER.info("---- Acquiring frame set "+n);
			try {
//				imagejJp4TiffMulti.getMultiImages( (n==0)? urls0:urls1, imps[n], telemetry,  scale,  std);
				imagejJp4TiffMulti.getMultiImages( (n==0)? urls0:urls1, imps[n],  scale,  std);
			} catch (IOException e) {
				LOGGER.error("readAllMultiple0: IOException, n = " + n);
			} catch (FormatException e) {
				LOGGER.error("readAllMultiple0:FormatException, n = " + n);
			}
		}
		double [][] img_seconds = new double [num_frames][BASE_URLS.length];
		int [][]    img_numbers = new int [num_frames][BASE_URLS.length];
		String [][] img_names = new String [num_frames][BASE_URLS.length];
		for (int n = 0; n < num_frames; n++) {
			for (int i = 0; i < imps[n].length; i++) {
				String dt = null;
				try {
					dt = (String) imps[n][i].getProperty("DATE_TIME");
				} catch (Exception e) {
					LOGGER.error("Something is wrong!");
				}
				if (dt==null) {
					System.out.println("n="+n+", i="+i+", dt is NULL! (may need to reduce number of acquire images to "+(n-1));
				}
//				System.out.println("n="+n+", i="+i+", dt="+dt);
				img_seconds[n][i] = Double.parseDouble(dt.substring(dt.lastIndexOf(":")+1));
				try {
					img_numbers[n][i] = Integer.parseInt((String) imps[n][i].getProperty("STD_Image_Number"));
				} catch (Exception e) {
					img_numbers[n][i] = -1;
				}
				img_names[n][i] = (String) imps[n][i].getProperty("CONTENT_FILENAME");
				LOGGER.warn("Seconds for" + n+":"+i+" - "+img_seconds[n][i]+", number"+img_numbers[n][i]+", name "+img_names[n][i]);
			}
		}


		for (ImagePlus [] imp_sets :imps) {
			for (ImagePlus imp: imp_sets) {
//				imp.show();
			}
		}
		return imps;
	}

	public ImagePlus [] averageMultiFrames(
			final ImagePlus [][] sets) {
		final int num_frames = sets.length;
		final int num_channels = sets[0].length;
		final ImagePlus [] imps_avg = new ImagePlus [num_channels];
   		final Thread[] threads = newThreadArray(MAX_THREADS);
   		final AtomicInteger indxAtomic = new AtomicInteger(0);

   		for (int ithread = 0; ithread < threads.length; ithread++) {
   			threads[ithread] = new Thread() {
   				@Override
				public void run() {
   					for (int chn = indxAtomic.getAndIncrement(); chn < num_channels; chn = indxAtomic.getAndIncrement())
   					{
   						int width =  sets[0][chn].getWidth();
   						int height = sets[0][chn].getHeight();
   						String title = sets[0][chn].getTitle()+"_average"+num_frames;
   						float [] pixels_avg = (float []) sets[0][chn].getProcessor().getPixels(); //null pointer
   						for (int n = 1; n < num_frames; n++) {
   							float [] pixels = (float []) sets[n][chn].getProcessor().getPixels();
   							for (int i = 0; i < pixels_avg.length; i++) {
   								pixels_avg[i] += pixels[i];
   							}
   						}
   						double scale = 1.0/num_frames;
   						for (int i = 0; i < pixels_avg.length; i++) {
   							pixels_avg[i] *= scale;
   						}
   						ImageProcessor ip=new FloatProcessor(width,height);
   						ip.setPixels(pixels_avg);
   						ip.resetMinAndMax();
   						imps_avg[chn]=  new ImagePlus(title, ip);
   						Properties properties0 = sets[0][chn].getProperties();
   						for (String key:properties0.stringPropertyNames()) {
   							imps_avg[chn].setProperty(key, properties0.getProperty(key));
   						}
   						imps_avg[chn].setProperty("average", ""+num_frames);
   						if (motorsPosition!=null) for (int m=0;m<motorsPosition.length;m++ ) {
   							imps_avg[chn].setProperty("MOTOR"+(m+1), ""+motorsPosition[m]);
   						}
   						ImagejJp4Tiff.encodeProperiesToInfo(imps_avg[chn]);
   					}
   				}
   			};
   		}
   		startAndJoin(threads);

   		/*
		for (int chn = 0; chn < num_channels; chn++) {
			int width =  sets[0][chn].getWidth();
			int height = sets[0][chn].getHeight();
			String title = sets[0][chn].getTitle()+"_average"+num_frames;
			float [] pixels_avg = (float []) sets[0][chn].getProcessor().getPixels(); //null pointer
			for (int n = 1; n < num_frames; n++) {
				float [] pixels = (float []) sets[n][chn].getProcessor().getPixels();
				for (int i = 0; i < pixels_avg.length; i++) {
					pixels_avg[i] += pixels[i];
				}
			}
			double scale = 1.0/num_frames;
			for (int i = 0; i < pixels_avg.length; i++) {
				pixels_avg[i] *= scale;
			}
			ImageProcessor ip=new FloatProcessor(width,height);
			ip.setPixels(pixels_avg);
			ip.resetMinAndMax();
			imps_avg[chn]=  new ImagePlus(title, ip);
			Properties properties0 = sets[0][chn].getProperties();
			for (String key:properties0.stringPropertyNames()) {
				imps_avg[chn].setProperty(key, properties0.getProperty(key));
			}
			imps_avg[chn].setProperty("average", ""+num_frames);
			if (motorsPosition!=null) for (int m=0;m<motorsPosition.length;m++ ) {
				imps_avg[chn].setProperty("MOTOR"+(m+1), ""+motorsPosition[m]);
			}
			ImagejJp4Tiff.encodeProperiesToInfo(imps_avg[chn]);
			// TODO: Overwrite some properties?
		}
   		 */
		return imps_avg;
	}

	public ImagePlus [][] matchSets(ImagePlus [][] sets, double max_mismatch, int max_frame_diff){
		return matchSets(sets, max_mismatch, max_frame_diff, null);
	}
	public ImagePlus [][] matchSets(ImagePlus [][] sets, double max_mismatch, int max_frame_diff, int [] lags){
		int num_frames = sets.length;
		int num_channels = sets[0].length;
		double [][] img_seconds = new double [num_frames][num_channels];
		int [] frame_offsets = new int[num_channels];
		double [] time_offsets = new double [num_channels];
		boolean some_notsynced = false;
		int fr_min = 0, fr_max = 0;
		frame_offsets[0] = 0;
		for (int n = 0; n < num_frames; n++) {
			for (int i = 0; i < num_channels; i++) {
				String dt = (String) sets[n][i].getProperty("DATE_TIME");
				img_seconds[n][i] = Double.parseDouble(dt.substring(dt.lastIndexOf(":")+1));

			}
		}
		for (int i = 1; i < num_channels; i++) { // compare all to the first channel
			frame_offsets[i] = 0;
			time_offsets[i] = secOffs(img_seconds[0][0], img_seconds[0][i]);
			if (num_frames > max_frame_diff) {
				if (time_offsets[i] > max_mismatch) {
					for (int fr_diff = 1; fr_diff <= max_frame_diff; fr_diff++) {
						double aoff = secOffs(img_seconds[fr_diff][0], img_seconds[0][i]);
						if (aoff < time_offsets[i]) {
							time_offsets[i] = aoff;
							frame_offsets[i] = fr_diff; // second channel starts later, than [0]
						}
						if (time_offsets[i] <=max_mismatch) break;
						aoff = secOffs(img_seconds[0][0], img_seconds[fr_diff][i]);
						if (aoff < time_offsets[i]) {
							time_offsets[i] = aoff;
							frame_offsets[i] = -fr_diff; // second channel starts earlier, than [0]
						}
						if (time_offsets[i] <=max_mismatch) break;
					}
				}
			}
			if (time_offsets[i] > max_mismatch) {
				some_notsynced = true;
			}
			if (frame_offsets[i] > fr_max) 	fr_max = frame_offsets[i];
			if (frame_offsets[i] < fr_min)  fr_min = frame_offsets[i];
		}
		if (some_notsynced) {
			LOGGER.error("*** Some channels are not synchronized, reboot or sensors re-start is needed ***");
			out_of_sync = true;
			for (int i = 0; i < num_channels; i++) {
				LOGGER.error("Channel "+ i+" frame offset="+frame_offsets[i]+ ", time offset = "+time_offsets[i]+" sec");
			}
			return null;
		}

		if (((fr_max - fr_min) > max_frame_diff) || ((fr_max - fr_min) > (num_frames - 1))) {
			out_of_sync = true;
			LOGGER.error("*** Earliest/latest channels differ by more than "+max_frame_diff+" frames, that should not happen! ***");
			for (int i = 0; i < num_channels; i++) {
				LOGGER.error("Channel "+ i+" frame offset="+frame_offsets[i]+ ", time offset = "+time_offsets[i]+" sec");
			}
			return null;
		}
		out_of_sync = false;
		for (int i = 0; i < num_channels; i++) {
			// change to info later:
			LOGGER.warn("Channel "+ i+" frame offset="+frame_offsets[i]+ ", time offset = "+time_offsets[i]+" sec");
		}
		// 		if (lags == null) lags = new int [num_channels];

		// recalculate  frame_offsets, fr_max, fr_min considering provided lags (>0 - channel images are acquired later)
		if (lags != null) {
			fr_max = lags[0];
			fr_min = lags[0];
			for (int i = 0; i < num_channels; i++ ) {
				frame_offsets[i] += lags[i];
				if (frame_offsets[i] > fr_max) 	fr_max = frame_offsets[i];
				if (frame_offsets[i] < fr_min)  fr_min = frame_offsets[i];
			}
		}
		ImagePlus [][] imps_synced = null;

		imps_synced = new ImagePlus [num_frames - fr_max + fr_min][num_channels];
		for (int n = 0; n < imps_synced.length; n++) {
			for (int i = 0; i < num_channels; i++) {
				imps_synced[n][i]= sets[n + fr_max -frame_offsets[i]][i];
			}
		}
		return imps_synced;
	}

	public boolean calibrate(LwirReaderParameters lrp) {
		// FFC takes 15 frames that are not output (so no need to check operation is over)
		if (lrp.lwir_channels.length == 0) {
			LOGGER.error("calibrate(): No LWIR channels are configured");
			return false;
		}
		// Skipping frame to synchronize setting calibration command
		if (!skipFrame(lrp)) {
			LOGGER.error("programLWIRCamera():Failed to skip frame");
		}

		int chn = lrp.lwir_channels[0];
		int channels = 0;
		for (int c : lrp.lwir_channels) {
			channels |= 1 << c;
		}
		String hex_chan = String.format("0x%x", channels);

		String url = "http://"+lrp.getLwirIP()+"/parsedit.php?immediate&sensor_port="+chn+
				"&SENSOR_REGS67=0*"+FRAMES_AHEAD+"!"+hex_chan;
//		String url = "http://"+lrp.getLwirIP()+"/parsedit.php?immediate&sensor_port="+chn+
//				"&SENSOR_REGS67=0&*SENSOR_REGS67="+hex_chan;
		Document dom=null;
		LOGGER.warn("calibrate(): Perform calibration (instead of 15 frames), url="+url);
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			dom = db.parse(url);
			if (!dom.getDocumentElement().getNodeName().equals("parameters")) {
				LOGGER.error("programLWIRCamera() in " + url+
						": Root element: expected 'parameters', got \"" + dom.getDocumentElement().getNodeName()+"\"");
				return false;
			}
		} catch(MalformedURLException e){
			LOGGER.error("programLWIRCamera() in " + url+ ": " + e.toString());
			return false;
		} catch(IOException  e1){
			LOGGER.error("programLWIRCamera() in " + url+ " - camera did not respond: " + e1.toString());
			return false;
		}catch(ParserConfigurationException pce) {
			LOGGER.error("programLWIRCamera() in " + url+ " - PCE error: " + pce.toString());
			return false;
		}catch(SAXException se) {
			LOGGER.error("programLWIRCamera() in " + url+ " - SAX error: " + se.toString());
			return false;
		}
		for (int i = 0; i < (FRAMES_SKIP+FRAMES_AHEAD); i++) {
			if (!skipFrame(lrp)) {
				LOGGER.error("programLWIRCamera():Failed to skip frame");
			}
		}

		return true;
	}


	private double secOffs(double sec1, double sec2) {
		double aoff = Math.abs(sec2 - sec1);
		if (aoff > 30) {
			aoff = Math.abs(aoff - 30);
		}
		return aoff;
	}
	public boolean skipFrame() {
		return skipFrame(lwirReaderParameters);
	}

	public boolean skipFrame(LwirReaderParameters lrp) {
		if (lrp.lwir_channels.length == 0) {
			LOGGER.error("skipFrame(): No LWIR channels are configured");
			return false;
		}
		int chn = lrp.lwir_channels[0];
		String url =  "http://"+lrp.getLwirIP()+":"+IMGSRV_PORTS[chn]+SKIP_FRAME_URL;
			Document dom=null;
			try {
				DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
				DocumentBuilder db = dbf.newDocumentBuilder();
				dom = db.parse(url);
				if (!dom.getDocumentElement().getNodeName().equals("meta")) {
					LOGGER.error("skipFrame() in " + url+
							": Root element: expected 'meta', got \"" + dom.getDocumentElement().getNodeName()+"\"");
						return false;
				}
	   		} catch(MalformedURLException e){
				LOGGER.error("skipFrame() in " + url+ ": " + e.toString());
				return false;
	   		} catch(IOException  e1){
			LOGGER.error("skipFrame() in " + url+ " - camera did not respond: " + e1.toString());
			return false;
	   		}catch(ParserConfigurationException pce) {
			LOGGER.error("skipFrame() in " + url+ " - PCE error: " + pce.toString());
			return false;
	   		}catch(SAXException se) {
			LOGGER.error("skipFrame() in " + url+ " - SAX error: " + se.toString());
			return false;
	   		}
		return true;
	}

	public ImagePlus [] acquire(String dirpath) {
		return acquire(lwirReaderParameters, dirpath);
	}


	public ImagePlus [] acquire(LwirReaderParameters lrp, String dirpath) {
		if (!condProgramLWIRCamera(lrp)) {
			LOGGER.error("acquire(): failed to program cameras");
			return null;
		}
		if (lrp.lwir_ffc) {
			calibrate(lrp); // seems to work. test calibration duration and if any images are sent during calibration
		}
		int num_frames = lrp.avg_number + lrp.eo_lag + 2 * lrp.max_frame_diff;
		if (num_frames > lrp.num_frames) {
			LOGGER.warn("acquire(): Wanted to acquire "+num_frames+", but hard limit is set to "+lrp.num_frames+
					" TDOD: stop EO compressor after acquisition of the required number of frames") ;
			num_frames = lrp.num_frames;
		}
		ImagePlus [][] imps = readAllMultiple(
				num_frames,
//				lrp.lwir_telemetry,
				false, // final boolean show,
				lrp.eo_scale);
		if (imps == null) {
			LOGGER.error("acquire(): failed to acquire images");
			return null;
		}
		LOGGER.debug("LWIR_ACQUIRE: got "+imps.length+" image sets");
// Verify fresh FFC for all channels
		double [] after_ffc = new double [lrp.lwir_channels.length];
		for (int chn = 0; chn < after_ffc.length; chn++) {
			double uptime = Double.NaN, ffctime = Double.NaN;
			try {
				uptime =  Double.parseDouble(((String) imps[0][chn].getProperty("TLM_UPTIME")));
				ffctime = Double.parseDouble(((String) imps[0][chn].getProperty("TLM_FFC_TIME")));
				after_ffc[chn] = uptime-ffctime;
			} catch (Exception e) {
				LOGGER.warn("acquire(): TLM_UPTIME and/or TLM_FFC_TIME properties are not available for"+imps[0][chn].getTitle());
			}
			LOGGER.warn(String.format("LWIR channel %d time from FFC: %.3f", chn, after_ffc[chn]));
		}

		int [] lags = new int [lrp.lwir_channels.length + lrp.eo_channels.length];
		for (int i = 0; i < lags.length; i++) {
			lags[i] = (i >= lrp.lwir_channels.length) ? lrp.eo_lag : 0;
		}
		ImagePlus [][] imps_sync =  matchSets(
				imps,
				lrp.max_mismatch_ms * 0.001, // 0.001,
				lrp.max_frame_diff,// 3); // double max_mismatch)
				lags);
		if (imps_sync == null) {
			return null;
		}
        int num_keep = imps_sync.length;
        if (!lrp.avg_all && (lrp.avg_number < imps_sync.length)) {
        	num_keep = lrp.avg_number;
        }
		LOGGER.debug("LWIR_ACQUIRE: got "+imps_sync.length+", requested "+lrp.avg_number+", keeping " + num_keep);
		if (num_keep < imps_sync.length) {
			ImagePlus [][] imps_sync0 = imps_sync;
			imps_sync = new ImagePlus[num_keep][];
			for (int i = 0; i < num_keep; i++) {
				imps_sync[i] = imps_sync0[i];
			}
		}
		ImagePlus [] imps_avg = averageMultiFrames(imps_sync);



		if (dirpath != null) {
			File  f_dir = new File(dirpath);
			// get series path from the first channel file title
			String first_name = imps_sync[0][0].getTitle();
			String set_name = first_name.substring(0, first_name.lastIndexOf('_'));
			String set_path = dirpath+Prefs.getFileSeparator()+set_name;
			File  set_dir = new File(set_path);
			set_dir.mkdirs(); // including parent
			LOGGER.warn("Saving image set to: "+set_dir.getAbsolutePath());
			for (ImagePlus imp:imps_avg) {
				String fname = imp.getTitle();
				fname = fname.substring(0, fname.lastIndexOf('_')) + ".tiff"; // remove _average
   				FileSaver fs=new FileSaver(imp);
   				String path=set_path+Prefs.getFileSeparator()+fname;
   				IJ.showStatus("Saving "+path);
   				LOGGER.info("LWIR_ACQUIRE: 'Saving "+path );
   				fs.saveAsTiff(path);
			}
		}

		if (lrp.isShowImages()) {
			if (imps_avg != null) {
				for (ImagePlus imp: imps_avg) {
					imp.show();
				}
			}
		}
		return imps_avg;
	}

	// TODO: Implement LWIR restart

	// Program cameras only if parameters had changed (including those, that do not actually need to be programmed)
	public boolean condProgramLWIRCamera() {
		return condProgramLWIRCamera(lwirReaderParameters);
	}


	public boolean condProgramLWIRCamera(LwirReaderParameters lrp) {
		boolean ok = true;
		if ((last_programmed == null) || !last_programmed.equals(lrp)) {
			ok = programLWIRCamera(lrp);
			if (ok) {
				last_programmed = lrp.clone();
			}
		}
		return ok;
	}

	//actually (unconditionally) program cameras parameters
	public boolean programLWIRCamera() {
		return programLWIRCamera(lwirReaderParameters);
	}

	public boolean programLWIRCamera(LwirReaderParameters lrp) {
		int lwir_master_port = 0;
		int eo_master_port = 0;
		int num_lwir = lrp.lwir_channels.length;
		int num_eo = lrp.eo_channels.length;
		final String [] urls = new String [num_lwir + num_eo];
		for (int chn:lrp.lwir_channels) {
			urls[chn] = "http://"+lrp.getLwirIP()+"/parsedit.php?immediate&sensor_port="+chn+
					"&BITS=16"+
					"&COLOR="+COLOR_RAW+ // +"*0"; // raw mode - delay 0 - breaks compressor
					"&WOI_HEIGHT="+(LWIR_HEIGHT + (lrp.lwir_telemetry?LWIR_TELEMETRY_LINES:0));
			if (chn == lwir_master_port) {
				urls[chn] +="&TRIG=0*0"+
						"&TRIG_DELAY="+lrp.lwir_trig_dly+"*0"+
						"&TRIG_OUT=419157*0"+
						"&TRIG_BITLENGTH=31*0"+
						"&EXTERN_TIMESTAMP=1*0"+
						"&XMIT_TIMESTAMP=1*0";
			}
		}
		for (int chn:lrp.eo_channels) {
	   		int minExposure=10; // usec
	   		int maxExposure=1000000; //usec
	   		int minGain=(int) (0x10000*1.0);
	   		int maxGain=(int) (0x10000*15.75);
	   		int minScale=0;
	   		int maxScale=(int) (0x10000*4.0);
	   		int exposure= (int) (Math.round(1000*lrp.eo_exposure_ms * lrp.eo_exp_corr[chn]));
	   		int autoExposureMax= (int) (Math.round(1000*lrp.eo_max_autoexp_ms));
	   		int gain=     (int) (Math.round(0x10000*lrp.eo_gain_g));
	   		int rScale=   (int) (Math.round(0x10000*lrp.eo_gain_rg*lrp.eo_gcorr_rbgb[3*chn+0]));
	   		int bScale=   (int) (Math.round(0x10000*lrp.eo_gain_bg*lrp.eo_gcorr_rbgb[3*chn+1]));
	   		int gScale=   (int) (Math.round(0x10000*                 lrp.eo_gcorr_rbgb[3*chn+2]));
	   		int autoExp=  lrp.eo_autoexp?1:0;
	   		int autoWB=   lrp.eo_whitebal?1:0;
	   		if (exposure<minExposure) exposure=minExposure; else if (exposure>maxExposure) exposure=maxExposure;
	   		if (autoExposureMax<minExposure) autoExposureMax=minExposure; else if (autoExposureMax>maxExposure) autoExposureMax=maxExposure;
	   		if (gain<minGain) gain= minGain ; else if (gain> maxGain) gain= maxGain;
	   		if (rScale<minScale) rScale= minScale ; else if (rScale> maxScale) rScale= maxScale;
	   		if (bScale<minScale) bScale= minScale ; else if (bScale> maxScale) bScale= maxScale;
	   		if (gScale<minScale) gScale= minScale ; else if (gScale> maxScale) gScale= maxScale;

	   		urls[num_lwir+chn] = "http://"+lrp.eo_ip+"/parsedit.php?immediate&sensor_port="+chn+
	   				"&COLOR="+COLOR_JP4+ // "*1"+ // JP4 always
	   				"&QUALITY="+lrp.eo_quality+ // "*0"+
	   				"&EXPOS="+exposure+ // "*0"+
		   			"&AUTOEXP_EXP_MAX="+autoExposureMax+//"*0"+
		   			"&AUTOEXP_ON="+autoExp+//"*0"+
		   			"&GAING="+gain+//"*0"+
		   			"&RSCALE="+rScale+//"*0"+
		   			"&BSCALE="+bScale+//"*0"+
		   			"&GSCALE="+gScale+//"*0"+ // GB/G ratio
		   			"&WB_EN="+autoWB+//"*0"+
	   				"&DAEMON_EN_TEMPERATURE=1";//"*0";
	   		if (lrp.eo_full_window) {
	   			urls[num_lwir+chn] +=
	   					"&WOI_LEFT=0"+
	   					"&WOI_TOP=0"+
	   					"&WOI_WIDTH=2592"+
	   					"&WOI_HEIGHT=1936";
	   		}


	   		if (chn == eo_master_port) {
	   			urls[num_lwir+chn] += "&TRIG=4*0"+
	   					"&TRIG_CONDITION=611669*0"+ // external input
	   					"&TRIG_BITLENGTH=31*0"+
	   					"&EXTERN_TIMESTAMP=1*0";
	   		}
		}
		for (int i = 0; i < urls.length; i++) {
				LOGGER.debug("programLWIRCamera(): reading url " + urls[i]);
		}
// multithreaded camera access:
   		final Thread[] threads = newThreadArray(MAX_THREADS);
   		final AtomicInteger indxAtomic = new AtomicInteger(0);
   		final boolean [] get_success = new boolean [urls.length];
   		for (int ithread = 0; ithread < threads.length; ithread++) {
   			threads[ithread] = new Thread() {
   				@Override
				public void run() {
   					for (int indx = indxAtomic.getAndIncrement(); indx < urls.length; indx = indxAtomic.getAndIncrement()) {
   						Document dom=null;
   						try {
   							DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
   							DocumentBuilder db = dbf.newDocumentBuilder();
   							dom = db.parse(urls[indx]);
   							if (!dom.getDocumentElement().getNodeName().equals("parameters")) {
   								LOGGER.error("programLWIRCamera() in " + urls[indx]+
   										": Root element: expected 'parameters', got \"" + dom.getDocumentElement().getNodeName()+"\"");
   	   							get_success[indx] = false;
   	   							continue;
   							}
   				   		} catch(MalformedURLException e){
								LOGGER.error("programLWIRCamera() in " + urls[indx]+ ": " + e.toString());
	   							get_success[indx] = false;
	   							continue;
   				   		} catch(IOException  e1){
							LOGGER.error("programLWIRCamera() in " + urls[indx]+ " - camera did not respond: " + e1.toString());
   							get_success[indx] = false;
   							continue;
   				   		}catch(ParserConfigurationException pce) {
							LOGGER.error("programLWIRCamera() in " + urls[indx]+ " - PCE error: " + pce.toString());
   							get_success[indx] = false;
   							continue;
   				   		}catch(SAXException se) {
							LOGGER.error("programLWIRCamera() in " + urls[indx]+ " - SAX error: " + se.toString());
   							get_success[indx] = false;
   							continue;
   				   		}
   						get_success[indx] = true;
   					}
   				}
   			};
   		}
   		startAndJoin(threads);
// See if there are any errors
   		boolean allOK = true;
   		for (boolean OK:get_success) {
   			allOK &= OK;
   		}
   		if (allOK){
   			lrp.reset_updated();

   			for (int i = 0; i < FRAMES_SKIP; i++) {
   				if (!skipFrame(lrp)) {
					LOGGER.error("programLWIRCamera():Failed to skip frame");
   				}
   			}
   		}
		return allOK;
	}

	/* Create a Thread[] array as large as the number of processors available.
	 * From Stephan Preibisch's Multithreading.java class. See:
	 * http://repo.or.cz/w/trakem2.git?a=blob;f=mpi/fruitfly/general/MultiThreading.java;hb=HEAD
	 */
	private static Thread[] newThreadArray(int maxCPUs) {
		int n_cpus = Runtime.getRuntime().availableProcessors();
		if (n_cpus>maxCPUs)n_cpus=maxCPUs;
		return new Thread[n_cpus];
	}
/* Start all given threads and wait on each of them until all are done.
	 * From Stephan Preibisch's Multithreading.java class. See:
	 * http://repo.or.cz/w/trakem2.git?a=blob;f=mpi/fruitfly/general/MultiThreading.java;hb=HEAD
	 */
	private static void startAndJoin(Thread[] threads)
	{
		for (int ithread = 0; ithread < threads.length; ++ithread)
		{
			threads[ithread].setPriority(Thread.NORM_PRIORITY);
			threads[ithread].start();
		}

		try
		{
			for (int ithread = 0; ithread < threads.length; ++ithread)
				threads[ithread].join();
		} catch (InterruptedException ie)
		{
			throw new RuntimeException(ie);
		}
	}


}