package com.elphel.imagej.orthomosaic;

import java.awt.Color;
import java.awt.Font;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.common.GenericJTabbedDialog;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.gpu.TpTask;
import com.elphel.imagej.ims.Imx5;
import com.elphel.imagej.readers.ElphelTiffReader;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.IntersceneMatchParameters;
import com.elphel.imagej.tileprocessor.QuadCLT;

import Jama.Matrix;
import ij.ImagePlus;
import ij.ImageStack;
import ij.Prefs;
import ij.gui.PointRoi;
import ij.plugin.filter.AVI_Writer;
import ij.plugin.filter.GaussianBlur;
import ij.process.FloatProcessor;
import ij.process.ImageConverter;
import ij.process.ImageProcessor;

public class OrthoMap implements Comparable <OrthoMap>, Serializable{
	private static final long serialVersionUID = 1L;
	public static boolean FIX_VERT_Y = false; // true; // temporarily fix vertical Y coordinate bug (use -GCORR in the filename?)
	public static int gpu_width =  4096;
	public static int gpu_height =  4096;
	public static final String [] KEY_DIRS= {"rootDirectory", // from EyesisCorrectionParameters
			"sourceDirectory","linkedModels","videoDirectory","x3dDirectory","resultsDirectory"};
	public static final String ALT_SUFFIX = "-ALT";
//	public transient final String               name;  // timestamp
//	public transient final double               ts;
	public transient String                     name;  // timestamp
	public transient double                     ts;
	public transient String                     path;        // full path to the model directory (including /vXX?)
	public double []                            lla;      // lat/long/alt
	public LocalDateTime                        dt;
	// affine convert (input) rectified coordinates (meters) relative to vert_meters to source image
	// coordinates relative to vert_meters
	public double [][]                          affine = new double[][] {{1,0,0},{0,1,0}}; // relative to vert_meters[]
	public double                               orig_pix_meters;
	public double []                            vert_meters;   // offset of the image vertical in meters (scale-invariant)
	public transient FloatImageData             orig_image;
	public transient FloatImageData             alt_image;
	public int                                  orig_zoom_level;
	public boolean                              orig_zoom_valid;
	public double                               need_extra_zoom;
	transient HashMap <Integer, FloatImageData> images;
	HashMap <Double, PairwiseOrthoMatch>        pairwise_matches;
	
	
	private void writeObject(ObjectOutputStream oos) throws IOException {
		oos.defaultWriteObject();
		oos.writeObject(path);
		// lla is not transient
		// dt is not transient
		// affine is not transient
		// orig_pix_meters is not transient
		// vert_meters is not transient
		// orig_image does not need to be saved
		// alt_image does not need to be saved
		// orig_zoom_level is not transient
		// orig_zoom_valid is not transient
		// need_extra_zoom is not transient
		// images is not saved
		// pairwise_matches is not transient
	}

	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
		ois.defaultReadObject();
		path = (String) ois.readObject();
		name = getNameFromPath(path);
		ts= Double.parseDouble(name.replace("_", "."));
		// lla is not transient
		// dt is not transient
		// affine is not transient
		// orig_pix_meters is not transient
		// vert_meters is not transient
		// orig_image was not saved
		// alt_image was not saved
		images = new HashMap <Integer, FloatImageData>(); // field images was not saved
		// pairwise_matches is not transient
//		pairwise_matches = new HashMap<Double, PairwiseOrthoMatch>();
	}
	
    @Override
    public int compareTo(OrthoMap otherPlayer) {
        return Double.compare(ts, otherPlayer.ts);
    }
	
	public static void setGPUWidthHeight(
			int width,
			int height) {
		gpu_width = width;
		gpu_height = height;		
	}

// Generate ALT image path from the GEO
	public static String getAltPath(String path) {
		int p1 = path.lastIndexOf(".");
		return path.substring(0,p1)+ALT_SUFFIX+".tiff";
		
	}
	public static String getNameFromPath(String path) {
		int p1 = path.lastIndexOf(Prefs.getFileSeparator());
		if (p1 < 0) return null;
		int p2 = path.indexOf("-", p1+1);
		if (p2 < 0) return null;
		return path.substring(p1+1, p2);
	}
	
	public OrthoMap (String path) {
		this.path = path;
		name = getNameFromPath(path);
		ts= Double.parseDouble(name.replace("_", "."));
		Properties imp_prop = null;
		try {
			imp_prop = ElphelTiffReader.getTiffMeta(path);				
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		lla = ElphelTiffReader.getLLA(imp_prop);
		dt = ElphelTiffReader.getLocalDateTime(imp_prop);
		vert_meters =      ElphelTiffReader.getXYOffsetMeters(imp_prop);
		orig_pix_meters = ElphelTiffReader.getPixelSize(imp_prop)[0];
		if (FIX_VERT_Y) {
			int height_pix =   ElphelTiffReader.getHeight(imp_prop);
			double height_meters = height_pix * orig_pix_meters;
			vert_meters[1] = height_meters-vert_meters[1];
		}
		
		orig_zoom_level = FloatImageData.getZoomLevel(orig_pix_meters);
		orig_zoom_valid = FloatImageData.isZoomValid(orig_pix_meters);
		need_extra_zoom = FloatImageData.needZoomIn(orig_pix_meters);
		images = new HashMap <Integer, FloatImageData>();
		pairwise_matches = new HashMap<Double, PairwiseOrthoMatch>();
	}
	
	
	public LocalDateTime getLocalDateTime() {
		return dt;
	}
	
	public String getName() {
		return name;
	}
	public void setAffine(double [][] affine) {
		this.affine = affine;
	}
	public double [][] getAffine(){
		return affine;
	}

	public void removeImageData() { // to save memory
		orig_image=null;
	}
	
	public void removeAltData() { // to save memory
		alt_image = null;
	}
	
	public void removeZoomLevel(int zoom_level) { // either to save memory or remove to later replace
		images.remove(zoom_level);
	}
	
	public int [] getZoomLevels() {
		Integer [] ks = (Integer[]) images.keySet().toArray();
		int [] zoom_levels = new int[ks.length];
		for (int i = 0; i < ks.length; i++) {
			zoom_levels[i] = ks[i];
		}
		return zoom_levels;
	}
	
	public void removeAllButZoomLevel(int zoom_level) { // either to save memory or remove to later replace
		int [] zoom_levels = getZoomLevels();
		for (int zl:zoom_levels) if (zl !=zoom_level) {
			images.remove(zl);
		}
	}

	
	public FloatImageData getImageData() {
		if (orig_image == null) {
			readImageData();
		}
		return orig_image;
	}
	public FloatImageData getAltData() {
		if (alt_image == null) {
			readAltData();
		}
		return alt_image;
	}
	
	private boolean readImageData() {
		ImagePlus imp = new ImagePlus(path);
		int width = imp.getWidth();
		int height = imp.getHeight();
		
		// TODO: FIXME!
//		vert_meters[1] = height-vert_meters[1];
		orig_image = new FloatImageData (
				width, // int       width,
				height, // int       height,
				orig_zoom_level, // int       zoom_lev,
				vert_meters, // double [] vert, // x,y pixel offset of the point under the camera
				(float[]) (imp.getProcessor().getPixels())); //    data);
		if (orig_zoom_valid) {
			images.put(orig_zoom_level, orig_image);
		}
		return true;
	}

	private boolean readAltData() { // assuming the same scale as main image
		ImagePlus imp = new ImagePlus(getAltPath(path));
		int width = imp.getWidth();
		int height = imp.getHeight();
		alt_image = new FloatImageData (
				width, // int       width,
				height, // int       height,
				orig_zoom_level, // int       zoom_lev,
				vert_meters, // double [] vert, // x,y pixel offset of the point under the camera
				(float[]) (imp.getProcessor().getPixels())); //    data);
		return true;
	}
	
	public ImagePlus getOriginalImage(boolean show_markers) {
		FloatImageData orig_image = getImageData();
		if (orig_image != null) {
			String full_name = path.substring(path.lastIndexOf(Prefs.getFileSeparator()) + 1);
			ImagePlus imp = ShowDoubleFloatArrays.makeArrays(
					orig_image.data, // float[] pixels,
					orig_image.width,
					orig_image.height,
					full_name);
			if (show_markers) {
				PointRoi roi = new PointRoi();
				roi.addPoint(orig_image.vert[0],orig_image.vert[1]);
				roi.setOptions("label");
				imp.setRoi(roi);
			}
			return imp;
		}
		return null;
	}
	
	public double [] enuOffsetTo(OrthoMap other) {
		return Imx5.enuFromLla(other.lla, lla);
	}
	
	public static double getPixelSizeMeters (int zoom_level ) {
		double ps = 0.01; // 1 cm;
		if (zoom_level >0) {
			for (int i = 0; i < zoom_level; i++) {
				ps /= 2;
			}
		} else {
			for (int i = 0; i > zoom_level; i--) {
				ps *= 2;
			}
		}
		return ps;
	}
	//	private double [][]               affine = new double[2][3];
	// vert_meters =     ElphelTiffReader.getXYOffsetMeters(imp_prop);
	
	
	/**
	 * Get top-left, top-right, bottom-right, bottom-left corners coordinates relative
	 * to a "vertical" point. {{-,-},{+,-},{+,+},{-,+}} 
	 * @return double [4][2] array [corner number]{x,y} 
	 */
	public double [][] get4SourceCornersMeters(){
		FloatImageData orig_image = getImageData();
		double width_meters = orig_image.width * orig_pix_meters;
		double height_meters = orig_image.height * orig_pix_meters;
		return new double[][] { // CW from TL
			{             - vert_meters[0],               - vert_meters[1]},
			{width_meters - vert_meters[0],               - vert_meters[1]},
			{width_meters - vert_meters[0], height_meters - vert_meters[1]},
			{             - vert_meters[0], height_meters - vert_meters[1]}};
	}
	
	/**
	 * Get metric bounds of this image (zero point at vert_meters)
	 * @param rectified if true, use rectified (inverse-transformed) image, false - original  
	 * @return rectified {{x_min, x_max},{y_min,y_max}} 
	 */
	public double [][] getBoundsMeters (boolean rectified){
		double [][] corners = get4SourceCornersMeters();
		if (rectified) {
			double [][] inv_aff = invertAffine(affine);
			double [][] rec_corners = new double [4][2];
			for (int n = 0; n < rec_corners.length; n++) {
				rec_corners[n][0] = inv_aff[0][0]*corners[n][0] + inv_aff[0][1]*corners[n][1] + inv_aff[0][2]; 
				rec_corners[n][1] = inv_aff[1][0]*corners[n][0] + inv_aff[1][1]*corners[n][1] + inv_aff[1][2]; 
			}
			corners = rec_corners;
		}
		double [][] bounds = new double [2][2]; // rows: x,y. Columns: min,max.
		for (int n = 0; n < 2; n++) {
			bounds[n][0] = corners[0][n];
			bounds[n][1] = bounds[n][0];
			for (int i = 1; i < corners.length; i++) {
				bounds[n][0]=Math.min(bounds[n][0], corners[i][n]);
				bounds[n][1]=Math.max(bounds[n][1], corners[i][n]);
			}
		}
		return bounds;
	}
	
	/**
	 * Get pixel bounds of this image (zero point at vert_meters) as doubles (to be able to
	 * offset before converting to int.
	 * @param rectified if true, use rectified (inverse-transformed) image, false - original  
	 * @return rectified {{x_min, x_max},{y_min,y_max}} 
	 */
	public double [][] getBoundPixels(
			int zoom_level,
			boolean rectified){
		double [][] bounds_meters = getBoundsMeters (rectified);
		double pix_size = getPixelSizeMeters (zoom_level);
		double [][] bounds_pix = new double[2][2];
		for (int n = 0; n < bounds_pix.length; n++) {
			for (int i = 0; i < 2; i++) {
				bounds_pix[n][i] = bounds_meters[n][i]/pix_size;
			}
		}
		return bounds_pix;
	}
	
	/**
	 * In preparation for the GPU correlation (zoomed out images of the fixed
	 * size are calculated separately and have NaN for unused pixels.
	 * Calculate overlap area bounds relative to the reference image origin
	 * (vertical point) and top-left (of the rectified overlap area) referenced
	 * affine matrices for both images ([2][2][3]).
	 * The overlap area should fit into gpu_width, gpu_hight, the affine-generated
	 * image coordinates will be limited during TpTask[][] generation 
	 * @param ref_map    OrthoMap instance for the reference map
	 * @param var_map    OrthoMap instance for the variable map instance
	 * @param zoom_lev   zoom level (0 - 1 pix=1cm, 1 - 1 pix = 0.5cm, -1 - 1 pix = 2 cm.
	 * @param var_offset additional (to affine and lla, vert_meters) x,y offset of
	 *                   the var_map (in output pixels). Used for spiral search for initial
	 *                   match.
	 * @param gpu_affine combined [2][2][3] array for convertion from the rectified overlap
	 *                   area to the GPU input images. Referenced to the top-left pixel
	 *                   of the rectified overlap area and top-left image pixels
	 * @return {{min_x,max_x}{min_y, max_y}) of the rectified area where (0,0) corresponds
	 *         (is centered) to the reference image origin (vertical point)  
	 */
	public static int [][] getPairOvelapBoundsAffine(
			OrthoMap ref_map,
			OrthoMap var_map,
			int      zoom_lev,
			int []   var_offset,
			double [][][] gpu_affine){
		return null;
	}

	/**
	 * Convert from the affine matrix that calculates image coordinates from the rectified one
	 * (relative to the origin point) to the matrix that provides rectified coordinates from the
	 * image ones. Both are in meters relative to the "vert_meters" point
	 * @param affine [2][3] array were the last column is made of the offsets (in meters)
	 * @return [2][3] array that converts from the image metric coordinates relative to the
	 * "vert_meters" point to the rectified metric coordinates relative to the same point.  
	 */
	public static double [][] invertAffine(double [][] affine){
		Matrix A = new Matrix (
				new double [][] {{affine[0][0],affine[0][1]},{affine[1][0],affine[1][1]}});
		Matrix Ai = A.inverse();
		Matrix B = new Matrix(new double [][] {{affine[0][2]},{affine[1][2]}});
		Matrix AiB = Ai.times(B);
		double [][] iaffine = {
				{Ai.get(0,0),Ai.get(0,1), -AiB.get(0,0)},
				{Ai.get(1,0),Ai.get(1,1), -AiB.get(1,0)}};
		return iaffine;
	}
	
	public boolean downScaleForGPU (int zoom_level){
		FloatImageData orig_image = getImageData();
		if (orig_image == null) {
			System.out.println("Original image is null");
			return false;
		}
		if (!orig_zoom_valid) {
			System.out.println("Original zoom level is invalid, need_extra_zoom = "+need_extra_zoom);
			return false;
		}
		if (zoom_level > orig_zoom_level) {
			System.out.println("Requested zoom level is too high ("+zoom_level+" > "+orig_zoom_level+")");
			return false;
		}
		if (images.get(zoom_level) != null) {
			return true; // already exists
		}
		int rscale = 1;
		for (int i = orig_zoom_level; i > zoom_level; i--) {
			rscale *= 2;
		}
		final int frscale = rscale;
		int swidth =  orig_image.width;
		int sheight = orig_image.height;
		final float [] spix = orig_image.data;
		final int width = (swidth+frscale-1)/frscale;
		final int height = (sheight+frscale-1)/frscale;
		final int tiles = width * height;
		final float [] opix = new float [tiles];
		Arrays.fill(opix, Float.NaN);
		final double [][] wnd = new double [frscale] [frscale];
		for (int i = 0; i <frscale; i++) {
			double w = Math.sin((i+0.5)/frscale*Math.PI);
			for (int j = 0; j <frscale; j++) {
				wnd[i][j] = w*Math.sin((j+0.5)/frscale*Math.PI);
			}
		}
		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                    for (int tile = ai.getAndIncrement(); tile < tiles; tile = ai.getAndIncrement()) {
                    	int tileX = tile%width;
                    	int tileY = tile/width;
                    	int tileW= (frscale*(tileX+1) <= swidth) ? frscale:(swidth - frscale*tileX);  
                    	int tileH= (frscale*(tileY+1) <= sheight) ? frscale:(sheight - frscale*tileY);
                    	double sw = 0.0, swd = 0.0;
                    	for (int py = 0; py < tileH; py++) {
                    		int ls = (tileY*frscale + py)*swidth;
                    		for (int px = 0; px < tileW; px++) {
                    			double d = spix[ls + tileX*frscale+px];
                    			if (!Double.isNaN(d)) {
                    				double w = wnd[py][px];
                    				sw += w;
                    				swd += w*d;
                    			}
                    			if (sw > 0) {
                    				opix[tileY * width + tileX] = (float) (swd/sw);
                    			}
                    		}
                    	}
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
        images.put(zoom_level, new FloatImageData (
				width, // int       width,
				height, // int       height,
				zoom_level, // int       zoom_lev,
				vert_meters, // double [] vert, // x,y pixel offset of the point under the camera
				opix)); //    data);
		return true;
	}
	
	/**
	 * Copy prepared scaled image to the top-left corner of the fixed-size
	 * float[] array for the GPU input
	 * @param zoom_level
	 * @return
	 */
	public float [] getPaddedGPU (
			int zoom_level){
		boolean got_image = downScaleForGPU (zoom_level); // will create if does not exist
		if (!got_image) {
			System.out.println("getPaddedGPU(): failed to prepare a scaled image");
			return null;
		}
		FloatImageData fid = images.get(zoom_level);
		
		final float [] padded_gpu = new float[gpu_width*gpu_height];
		final int swidth =  fid.width;
		final int sheight = fid.height;
		final float [] spix = fid.data;
		Arrays.fill(padded_gpu, Float.NaN);
		final int cheight = Math.min(sheight,gpu_height);
		final int cwidth =  Math.min(swidth, gpu_width);

		final Thread[] threads = ImageDtt.newThreadArray();
		final AtomicInteger ai = new AtomicInteger(0);
        for (int ithread = 0; ithread < threads.length; ithread++) {
            threads[ithread] = new Thread() {
                public void run() {
                    for (int row = ai.getAndIncrement(); row < cheight; row = ai.getAndIncrement()) {
        				System.arraycopy(
        						spix,
        						row*swidth,
        						padded_gpu,
        						row*gpu_width,
        						cwidth);
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
		return padded_gpu;
	}
	
	
	public static void testVideo(ImagePlus imp) {
		String path_prefix = "/media/elphel/SSD3-4GB/lwir16-proc/ortho_videos/videos/video_";
		boolean   keep_original =        true;
		boolean   um_apply =             true;
		double    um_sigma =             10.0;
		double    um_weight =            0.9;
		boolean   auto_range =           false;
		double    mono_range =           300.0; 
		boolean   annotate   =           true;
		Color     annotate_color =       new Color( 255, 180,  50);
		boolean   annotate_transparent = false;
		int       annotate_font_size =   36;
		boolean   annotate_font_bold =   true;
		double    video_fps =            10;
		boolean   avi_compress_jpeg =    false;
		int       aviJpegQuality =       95;
		boolean   run_ffmpeg =           true;
		String    video_ext =            ".webm";
		String    video_codec =          "vp8";
		int       video_crf =            10; // lower - better, larger file size
		boolean   remove_avi =           true;
		int       debugLevel =           0;
		GenericJTabbedDialog gd = new GenericJTabbedDialog("Parameters for video from orthographic imges",1200,800);
		gd.addStringField ("Output video path prefix",  path_prefix, 180,
				"Absolute path prefix for the video (image title will be added).");
		gd.addCheckbox    ("Keep original image", keep_original, "If true, will generate new RGB image, if false - replace existent.");
		gd.addCheckbox    ("Apply unsharp mask filter", um_apply, "Apply unsharp mask filter.");
		gd.addNumericField("Unsharp mask sigma", um_sigma,  3,7,"",	"Unsharp mask sigma.");
		gd.addNumericField("Unsharp mask weight", um_weight,  3,7,"",	"Unsharp mask weight.");
		gd.addCheckbox    ("Auto range while congerting to RGB", auto_range, "Autorange 32-bit floating point image when converting to RGB.");
		gd.addNumericField("Fixed range during conversion to RGB", mono_range,  3,7,"",	"Fixed range.");
		gd.addCheckbox    ("Annotate each frame with timestamp+date/time", annotate,
				"Embed annotation in the bottom-right corner of each frame of the converted image file and video.");
		{
			String scolor = (annotate_color==null)?"none":String.format("%08x", IntersceneMatchParameters.getLongColor(annotate_color));
			gd.addStringField ("Annotation color (hex number)",scolor, 8, "Any invalid hex number disables annotation");
		}
		gd.addCheckbox    ("Annotation transparent background", annotate_transparent, "Put annotation text directly over images.");
		gd.addNumericField("Annotation font size", annotate_font_size, 0, 4, "", "Annotation font size.");
		gd.addCheckbox    ("Bold annotation", annotate_font_bold, "Use bold font for annotations.");
		gd.addNumericField("Video output frame rate", video_fps,  3,7,"fps", "Frame rate of the video.");
		gd.addCheckbox    ("Compress AVI with JPEG", avi_compress_jpeg, "Use JPEG for AVI compression (false - use raw).");
		gd.addNumericField("AVI JPEG quality", aviJpegQuality, 0, 4, "", "AVI JPEG quality if JPEG compression is used.");
		gd.addCheckbox    ("Convert AVI to WEBM", run_ffmpeg, "Use ffmped to convert intermediate AVI video to WEBM.");
		gd.addStringField ("WEBM output extension",  video_ext, 5,"WEBM output file extension including dot, normally \".webm\".");
		gd.addStringField ("WEBM codec",  video_codec, 5,"WEBM codec \"vp8\" or \"vp9\"(vp9 had problems).");
		gd.addNumericField("WEBM CRF", video_crf, 0, 4, "", "WEBM compression quality (lower - better, 10 - good).");
		gd.addCheckbox    ("Remove AVI", remove_avi, "Remove AVI (large file) after WEBM generation.");
		gd.addNumericField("Debug level", debugLevel, 0, 4, "", "Debug level (not yet used).");
		gd.showDialog();
		if (gd.wasCanceled()) return;
		path_prefix =          gd.getNextString();
		keep_original =        gd.getNextBoolean();
		um_apply =             gd.getNextBoolean();
		um_sigma=              gd.getNextNumber();
		um_weight=             gd.getNextNumber();
		auto_range =           gd.getNextBoolean();
		mono_range =           gd.getNextNumber();
		annotate =             gd.getNextBoolean();
        {
			String scolor =             gd.getNextString();
			long lcolor = -1;
			try {
				lcolor = Long.parseLong(scolor,16);
				annotate_color = IntersceneMatchParameters.setLongColor(lcolor);
			} catch(NumberFormatException e){
				annotate_color = null;
			}
        }
		annotate_transparent =     gd.getNextBoolean();
		annotate_font_size = (int) gd.getNextNumber();		
		annotate_font_bold =       gd.getNextBoolean();
		video_fps =                gd.getNextNumber();
		avi_compress_jpeg =        gd.getNextBoolean();
		aviJpegQuality =     (int) gd.getNextNumber();
		run_ffmpeg =               gd.getNextBoolean();
		video_ext =                gd.getNextString();
		video_codec =              gd.getNextString();
		video_crf =          (int) gd.getNextNumber();
		remove_avi =               gd.getNextBoolean();
		debugLevel =         (int) gd.getNextNumber();
		
		ImagePlus imp_um;
		if (um_apply) {
			imp_um = applyUnsharpMask(
					imp,           // final ImagePlus imp,
					keep_original, // final boolean   keep_original,
					um_sigma,      // 10,            // final double    um_sigma,
					um_weight);    // 0.9);          // final double    um_weight
		} else {
			imp_um = keep_original? imp.duplicate():imp;
		}
        System.out.println("applyUnsharpMask() DONE!");	
    	ImagePlus imp_rgb = convertToColorAnnotate(
    			imp_um,               // ImagePlus imp_src,
    			false,                // boolean   keep_original,
    			auto_range? 0:mono_range, // 300,  // double    mono_range,  // >0 - apply 
    			annotate,             // boolean   annotate,
    			annotate_color,       // Color.RED,  //Color    annotate_color,
    			annotate_transparent, // boolean   annotate_transparent,
    			annotate_font_size,   // int       annotate_font_size, // 12
    			annotate_font_bold,   // boolean   annotate_font_bold,
    			debugLevel);          // int       debugLevel)
    	imp_rgb.show();
        System.out.println("convertToColorAnnotate() DONE!");
        String path = path_prefix+imp_rgb.getTitle();
        int mode_avi = avi_compress_jpeg? 1 : 0;
        saveRGBSlicesAsVideo(
        		imp_rgb,        // ImagePlus  imp,
//    			0,              // int        col_mode, // 0 - mono, 1 - color
    			path,           // String     path,
    			video_fps,      // double     video_fps,
    			mode_avi,       // int        mode_avi, // 0 - raw, 1 - JPEG, 2 - PNG
    			aviJpegQuality, // int        aviJpegQuality,
    			run_ffmpeg,     // boolean    run_ffmpeg,
    			video_ext,      // ".webm",   // String     video_ext,
    			video_codec,    // "vp8",     // String     video_codec,
    			video_crf,      // 10,        // int        video_crf, // lower - better, larger file size
    			remove_avi,     // boolean    remove_avi,
    			debugLevel);    // final int  debugLevel
    			        
        System.out.println("saveRGBSlicesAsVideo() DONE!");
	}
	
	
	public static ImagePlus applyUnsharpMask(
			final ImagePlus imp_src,
			final boolean   keep_original, 
			final double    um_sigma,
			final double    um_weight
			) {
		final float fum_weight=(float) um_weight;
		final ImagePlus imp = keep_original? imp_src.duplicate() : imp_src;
		final ImageStack fstack_scenes = imp.getStack();
		final int nSlices = fstack_scenes.getSize();
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSlice = ai.getAndIncrement(); nSlice < nSlices; nSlice = ai.getAndIncrement()) {
						FloatProcessor fp = (FloatProcessor) fstack_scenes.getProcessor(nSlice+1);
						float [] fpixels = (float[]) fstack_scenes.getPixels(nSlice+1);
						float [] fpixels_orig = fpixels.clone();
						(new GaussianBlur()).blurFloat(
								fp,       // FloatProcessor ip,
								um_sigma, // double sigmaX,
								um_sigma, // double sigmaY,
								0.01);    // double accuracy)
						for (int i = 0; i < fpixels.length; i++) {
							fpixels[i] = fpixels_orig[i] - fum_weight * fpixels[i];
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		imp.setTitle(removeKnownExtension(imp.getTitle())+String.format("-UM%.1f_%.2f",um_sigma,um_weight));
		return imp;
	}
	
	public static ImagePlus convertToColorAnnotate(
			ImagePlus imp_src,
			boolean   keep_original,
			double    mono_range,  // >0 - apply 
			boolean   annotate,
			Color     annotate_color,
			boolean   annotate_transparent,
			int       annotate_font_size, // 12
			boolean   annotate_font_bold,
			int       debugLevel) {
		final ImagePlus imp = keep_original? imp_src.duplicate() : imp_src;
		if (imp.getType() != ImagePlus.COLOR_RGB) {
			if (mono_range > 0) {
				imp.getProcessor().setMinAndMax(-mono_range/2, mono_range/2);
			}
			ImageConverter imageConverter = new ImageConverter(imp);
			imageConverter.convertToRGB();
		}
		final Color fcolor = annotate_color;
		final ImageStack fstack_scenes = imp.getImageStack();
		int title_len = 0;
		final int nSlices = fstack_scenes.getSize();
		for (int i = 0; i < nSlices; i++) {
			if (fstack_scenes.getSliceLabel(i+1).length() > title_len) {
				title_len=fstack_scenes.getSliceLabel(i+1).length();
			}
		}
		final int annotate_size = title_len; //
		final int width =  imp_src.getWidth();
		final int height = imp_src.getHeight();
		final double char_wtoh = 0.6;
		final int posX= width - ((int) (char_wtoh* annotate_size * annotate_font_size)); //  119; // 521;
		final int posY= height + 1;  // 513;
		final Font font = new Font("Monospaced", Font.PLAIN | (annotate_font_bold ? Font.BOLD: 0), annotate_font_size);
		final Thread[] threads = ImageDtt.newThreadArray(QuadCLT.THREADS_MAX);
		final AtomicInteger ai = new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSlice = ai.getAndIncrement(); nSlice < nSlices; nSlice = ai.getAndIncrement()) {
						String scene_title = fstack_scenes.getSliceLabel(nSlice+1);
						ImageProcessor ip = fstack_scenes.getProcessor(nSlice+1);
						ip.setColor(fcolor); // Color.BLUE);
						ip.setFont(font);
						if (!annotate_transparent) { // toRGB || !annotate_transparent_mono) {
							ip.drawString(scene_title, posX, posY,Color.BLACK);
						} else {
							ip.drawString(scene_title, posX, posY); // transparent
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		imp.setTitle(removeKnownExtension(imp.getTitle())+((mono_range > 0) ? String.format("_%.0f",mono_range):"_A"));
		return imp;
	}
	
	public static String removeKnownExtension(String path) {
		String [] remove_ext = {".tiff", ".tif", ".avi"};
		for (String ext:remove_ext) {
			if (path.endsWith(ext)) {
				path = path.substring(0,path.length()-ext.length());
			}
		}
		return path;

	}
	public static boolean saveRGBSlicesAsVideo(
			ImagePlus  imp,
//			int        col_mode, // 0 - mono, 1 - color
			String     path,
			double     video_fps,
			int        mode_avi, // compression
			int        aviJpegQuality,
			boolean    run_ffmpeg,
			String     video_ext,
			String     video_codec,
			int        video_crf,
			boolean    remove_avi,
			final int  debugLevel
			) {
		boolean generate_mapped = true; // false - here just simulate
		// no_combine, stereo_2_images, stereo_anaglyth
		String avi_path = removeKnownExtension(path)+String.format("-FPS%04.1f",video_fps)+".avi";
		imp.getCalibration().fps = video_fps;
		video:
		{
			try {
				(new AVI_Writer()).writeImage (
						imp, // ImagePlus imp,
						avi_path, // String path,
						mode_avi, // int compression,
						aviJpegQuality); //int jpegQuality)
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				break video;
			}
			System.out.println("saveAVIInModelDirectory(): saved "+avi_path);
			if (!run_ffmpeg) {
				break video; // webm not requested
			}

			String webm_path = avi_path.substring(0, avi_path.length()-4)+video_ext;
			// added -y not to as "overwrite y/n?"
			//ffmpeg -i input_file.mkv -c copy -metadata:s:v:0 stereo_mode=1 output_file.mkv 
			//https://ffmpeg.org/ffmpeg-formats.html
			//ffmpeg -i sample_left_right_clip.mpg -an -c:v libvpx -metadata stereo_mode=left_right -y stereo_clip.webm
			//anaglyph_cyan_red 
			String stereo_meta = ""; // used in OpticalFlow
			String shellCommand = String.format("ffmpeg -y -i %s -c %s -b:v 0 -crf %d %s %s",
					avi_path, video_codec, video_crf, stereo_meta, webm_path);
			Process p = null;
			if (generate_mapped) {
				int exit_code = -1;
				try {
					p = Runtime.getRuntime().exec(shellCommand);
				} catch (IOException e) {
					System.out.println("Failed shell command: \""+shellCommand+"\"");
				}
				if (p != null) {
					try {
						p.waitFor();
					} catch (InterruptedException e) {
						System.out.println("saveRGBSlicesAsVideo(): failed waiting for process to finish.");
						// TODO Auto-generated catch block
						e.printStackTrace();
						return false;
					}
					exit_code = p.exitValue();
				}
				System.out.println("Ran shell command: \""+shellCommand+"\" -> "+exit_code);
				// Check if webm file exists
				if ((exit_code != 0) || !(new File(webm_path)).exists()) {
					System.out.println("Failed to create : \""+webm_path+"\"");
					break video;
				}
			} else {
				System.out.println("Simulated shell command: \""+shellCommand);
			}
			if (remove_avi && generate_mapped) {
				(new File(avi_path)).delete();
				System.out.println("Deleted AVI video file: \""+avi_path+"\"");
			}
		}
		return true;
	}
}
