package com.elphel.imagej.orthomosaic;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

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 Jama.Matrix;
import ij.ImagePlus;
import ij.Prefs;
import ij.gui.PointRoi;

public class ComboMap {
	public final String               name;  // timestamp
	public String                     path;        // full path to the model directory (including /vXX?)
	public double []                  lla;      // lat/long/alt
	public LocalDateTime              dt;
	private 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 FloatImageData             orig_image;
	public int                        orig_zoom_level;
	public boolean                    orig_zoom_valid;
	public double                     need_extra_zoom;
	HashMap <Integer, FloatImageData> images;

// Generate ALT image path from the GEO
	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 ComboMap (String path) {
		this.path = path;
		name = getNameFromPath(path);
		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];
		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>();
	}
	
	public void setAffine(double [][] affine) {
		this.affine = affine;
	}
	public double [][] getAffine(){
		return affine;
	}
	public boolean readImageData() {
		ImagePlus imp = new ImagePlus(path);
		int width = imp.getWidth();
		int height = imp.getHeight();
		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;
	}
	public ImagePlus getOriginalImage(boolean show_markers) {
		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(ComboMap 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);
	public double [][] getBoundsMeters (boolean inverse){ // inverse - from source to rectified
		// orig_pix_meters
		double width_meters = orig_image.width * orig_pix_meters;
		double height_meters = orig_image.height * orig_pix_meters;
		double [][] corners = 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]}};
		double [][] aff_corners = new double [4][2];
		for (int n = 0; n < aff_corners.length; n++) {
			aff_corners[n][0] = affine[0][0]*corners[n][0] + affine[0][1]*corners[n][1] + affine[0][2]; 
			aff_corners[n][1] = affine[1][0]*corners[n][0] + affine[1][1]*corners[n][1] + affine[1][2]; 
		}
		double [][] bounds = new double [2][2]; // rows: x,y. Columns: min,max.
		for (int n = 0; n < 2; n++) {
			bounds[n][0] = aff_corners[0][0];
			bounds[n][1] = bounds[n][0];
			for (int i = 1; i < aff_corners.length; i++) {
				bounds[n][0]=Math.min(bounds[n][0], aff_corners[i][n]);
				bounds[n][1]=Math.max(bounds[n][1], aff_corners[i][n]);
			}
		}
		return bounds;
	}
	public static double [][] getBoundsMeters(ComboMap [] maps){ // maps[0] as a reference
		double [][] bounds = maps[0].getBoundsMeters(false);
		for (int nmap = 1; nmap < maps.length; nmap++) {
			double [][] bounds_other = maps[nmap].getBoundsMeters(false);
			double [] enuOffset = maps[0].enuOffsetTo(maps[nmap]);
			double [] rd = {enuOffset[0], -enuOffset[1]}; // {right,down} of the image 
			for (int n = 0; n < bounds.length; n++) {
				bounds[n][0] = Math.min(bounds[n][0],bounds_other[n][0]+ rd[n]);
				bounds[n][1] = Math.max(bounds[n][1],bounds_other[n][1]+ rd[n]);
			}
		}
		return bounds;
	}
	public static int [][] getBoundsPixels(
			ComboMap [] maps,
			int zoom_level){ // maps[0] as a reference
		double [][] bounds_meters = getBoundsMeters(maps);
		double pix_size = getPixelSizeMeters (zoom_level);
		int [][] bounds_pix = new int[2][2];
		for (int n = 0; n < bounds_pix.length; n++) {
			bounds_pix[n][0] = (int) Math.floor(bounds_meters[n][0]/pix_size);
			bounds_pix[n][1] = (int) Math.ceil (bounds_meters[n][1]/pix_size);
		}
		return bounds_pix;
	}

	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 void removeZoomLevel(int zoom_level) { // either to save memory or remove to later replace
		images.remove(zoom_level);
	}
	public boolean downScaleForGPU (int zoom_level){
		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++) {
			for (int j = 0; j <frscale; j++) {
				wnd[i][j] = Math.sin((i+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
	 * @param gpu_width
	 * @param gpu_height
	 * @return
	 */
	public float [] getPaddedGPU (
			int zoom_level,
			int gpu_width,
			int gpu_height){
		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;
		}
		final float [] padded_gpu = new float[gpu_width*gpu_height];
		final int swidth =  orig_image.width;
		final int sheight = orig_image.height;
		final float [] spix = orig_image.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 float [][] renderMulti (
			ComboMap [] maps,
			int zoom_level){ // maps[0] as a reference
		
		int [][] bounds = getBoundsPixels(
				maps,
				zoom_level);
		int width =  bounds[0][1] - bounds[0][0]; // bounds[x][0] - negative 
		int height = bounds[1][1] - bounds[1][0]; 
		final float [][] fpixels = new float [maps.length][width * height];
		for (int nmap = 0; nmap< maps.length; nmap++) {
			final int fnmap = nmap;
			Arrays.fill(fpixels[nmap], Float.NaN);
			final double scale = getPixelSizeMeters(maps[nmap].orig_zoom_level)/getPixelSizeMeters(zoom_level);
			double [][] inv_bounds = maps[nmap].getBoundsMeters(true);
			double [] enu_offset = maps[0].enuOffsetTo(maps[nmap]);
			final double [] scaled_out_center = { // xy center to apply affine to
					-bounds[0][0]+scale * enu_offset[0],
					-bounds[1][0]+scale * enu_offset[1]};
			final int [][] ibounds = new int [2][2];
			for (int n = 0; n< 2; n++) {
				ibounds[n][0] = (int) Math.floor(-bounds[n][0] + scale*(inv_bounds[n][0] + enu_offset[n]));
				ibounds[n][1] = (int) Math.ceil (-bounds[n][0] + scale*(inv_bounds[n][0] + enu_offset[n]));
			}
			final int local_width =  ibounds[0][1] - ibounds[0][0];
			final int local_height = ibounds[1][1] - ibounds[1][0];
			final int local_len = local_width * local_height; 
			double [][] src_bounds=maps[nmap].getBoundsMeters (false);
			final double [] src_center = {-src_bounds[0][0],-src_bounds[1][0]}; // x,y center offset in the source image
			final double [][] affine = maps[nmap].affine;
			final int src_width = maps[nmap].orig_image.width;
			final int src_height = maps[nmap].orig_image.height;
			final float [] src_img = maps[nmap].orig_image.data;
			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 nPix = ai.getAndIncrement(); nPix < local_len; nPix = ai.getAndIncrement()) {
							int opX = nPix % local_width + ibounds[0][0]; // absolute output pX, pY
							int opY = nPix / local_width + ibounds[1][0];
							double dX = (opX - scaled_out_center[0]) /scale; // in original image scale 
							double dY = (opY - scaled_out_center[1]) /scale;
							double [] xy_src = {
									affine[0][0]*dX+affine[0][1]*dY+affine[0][2]+src_center[0],
									affine[1][0]*dX+affine[1][1]*dY+affine[1][2]+src_center[1]};
							// limit to the source image
							if      (xy_src[0] < 0)               xy_src[0] = 0;
							else if (xy_src[0] >= (src_width-1))  xy_src[0] = src_width-2; // to be on the safe side
							if      (xy_src[1] < 0)               xy_src[1] = 0;
							else if (xy_src[1] >= (src_height-1)) xy_src[1] = src_height-2;
							int [] ixy_src = {(int) Math.floor(xy_src[0]), (int)Math.floor(xy_src[1]) };
							double [] kxy = {xy_src[0]-ixy_src[0], xy_src[1]-ixy_src[1]}; 
							double d00 = src_img[ixy_src[0] + ixy_src[1]*src_width];
							if (!Double.isNaN(d00)) {
								double d01 = src_img[ixy_src[0] + 1 + ixy_src[1]*src_width];
								double d10 = src_img[ixy_src[0] + (ixy_src[1] + 1) *src_width];
								double d11 = src_img[ixy_src[0] + 1 + (ixy_src[1] + 1) *src_width];
								double d = d00*(1.0 - kxy[0])*(1.0 - kxy[1])+
										   d01*       kxy[0] *(1.0 - kxy[1])+
										   d10*(1.0 - kxy[0])*       kxy[1]+
										   d11*       kxy[0] *       kxy[1];
								fpixels[fnmap][opX + opY*width] = (float) d;
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
		}
		return fpixels;
	}
	
}
