/**
 **
 ** FloatImageData.java - Float-precision raster image container for orthomosaic processing
 **
 ** Copyright (C) 2024 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  FloatImageData.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.orthomosaic;

import java.awt.Rectangle;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.common.PolynomialApproximation;
import com.elphel.imagej.readers.ElphelTiffReader;
import com.elphel.imagej.tileprocessor.ImageDtt;

import ij.ImagePlus;

public class FloatImageData implements 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             int                   width;
	public             int                   height;
	public             int                   zoom_lev;
	private            boolean               zoom_valid;
	public             String                path;
	public             Properties            properties; // serializable
	private            double []             lla;      // lat/long/alt
	public             LocalDateTime         dt;
	private            double []             vert = new double[2]; // x,y offset (in meters) of the point under the camera
	private            double                pix_meters;
	private            double                averagePixel=Double.NaN;
	private transient  float[]               data; // make transient
	
	public FloatImageData (
			String path) {
		this.path = path;
		try {
			properties = ElphelTiffReader.getTiffMeta(path);				
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try {
			lla = ElphelTiffReader.getLLA(properties);
		} catch (NullPointerException e) {
			System.out.println("No GPS data in "+path);
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		dt = ElphelTiffReader.getLocalDateTime(properties);
		vert =      ElphelTiffReader.getXYOffsetMeters(properties);
		pix_meters = ElphelTiffReader.getPixelSize(properties)[0];
		if (FIX_VERT_Y) {
			int height_pix =   ElphelTiffReader.getHeight(properties);
			double height_meters = height_pix * pix_meters;
			vert[1] = height_meters-vert[1];
		}
		width =  ElphelTiffReader.getWidth(properties);
		height = ElphelTiffReader.getHeight(properties);
		zoom_lev = getZoomLevel(pix_meters);
		zoom_valid = isZoomValid(pix_meters);
	}
	
	private void writeObject(ObjectOutputStream oos) throws IOException {
		oos.defaultWriteObject();
	}
	
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
		ois.defaultReadObject();
		data = null;
	}	
	
	public void updateLLA() {
		try {
			properties = ElphelTiffReader.getTiffMeta(path);				
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		try {
			lla = ElphelTiffReader.getLLA(properties);
		} catch (NullPointerException e) {
			System.out.println("No GPS data in "+path);
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	float [] readFData() {
		if (data == null) {
			ImagePlus imp = new ImagePlus(path);
			int width = imp.getWidth();
			int height = imp.getHeight();
			if ((width != this.width) || (height != this.height)) {
				throw new IllegalArgumentException (String.format("IJ image size does not match Exif one: (%d,%d) != (%d,%d)",
						width,height, this.width,this.height));
			}
			data = (float[]) (imp.getProcessor().getPixels());
		}
		return data;
	}
	
	public double [] getDData() {
		if (data==null) {
			readFData();
			if (data== null) {
				return null;
			}
		}
		final double [] ddata = new double [data.length];
		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 ipix = ai.getAndIncrement(); ipix < data.length; ipix = ai.getAndIncrement()) {
                    	ddata[ipix] = data[ipix];
                    }
                }
            };
        }		      
        ImageDtt.startAndJoin(threads);
		return ddata;
	}
	

	public double [] getLLA() {
		return lla; // null if does not exist, saved with data file 
	}
	
	public LocalDateTime getDT() {
		return dt;
	}
	
	public boolean isZoomValid() {
		if (!zoom_valid) {
			System.out.println("Original zoom level is invalid, need_extra_zoom = "+needZoomIn(pix_meters));
		}
		return zoom_valid;
	}
	
	/**
	 * Get vertical point offset from the top-left corner of the original orthoimage in pixels
	 * of the original resolution
	 * @return vertical point X,Y offset in pixels from the top-left image corner in original resolution
	 */
	public int [] getVertPixels() {
		double [] vm = getVertMeters();
		return new int [] {
				(int) Math.round(vm[0]/pix_meters),
				(int) Math.round(vm[1]/pix_meters)};
	}
	
	public double getPixMeters() {
		return pix_meters;
	}
	public FloatImageData (
			FloatImageData master,
			int       zoom_level,
			float[]   data) {
		this.width =        master.width;
		this.height =       master.height;
		this.zoom_valid =   master.zoom_valid;
		this.path =         master.path;
		this.properties =   master.properties;
		this.lla =          master.lla;
		this.dt =           master.dt;
		this.vert =         master.vert;
		this.pix_meters =   master.pix_meters;
		this.averagePixel = master.averagePixel;

		this.zoom_lev =     zoom_level;
		this.data =         data;
	}	

	public double getAveragePixel() {
		if (Double.isNaN(averagePixel)) {
			final float [] pixels =  readFData();
			final Thread[] threads = ImageDtt.newThreadArray();
			final AtomicInteger ai = new AtomicInteger(0);
			final AtomicInteger ati = new AtomicInteger(0);
			final double [] avg_arr =  new double [threads.length];
			final double [] npix_arr = new double [threads.length];
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					public void run() {
						int thread_num = ati.getAndIncrement();
						for (int ipix = ai.getAndIncrement(); ipix < pixels.length; ipix = ai.getAndIncrement()) {
							float p = pixels[ipix];
							if (!Float.isNaN(p)) {
								avg_arr[thread_num] += p;
								npix_arr[thread_num] +=1;
							}
						}
					}
				};
			}		      
			ImageDtt.startAndJoin(threads);
			double avg=0, num=0;
			for (int i = 0; i < avg_arr.length; i++) {
				avg+=avg_arr[i]; // *npix_arr[i];
				num+=npix_arr[i];
			}
			averagePixel = avg/num; 
		}
		return averagePixel;
	}

	
	
	
	public int getWidth() {
		return width;
	}
	public int getHeight() {
		return height;
	}
	
	public double[]  getVertMeters() {
		return vert;
	}
	
	public static int getZoomLevel(
			double pix_in_meters) {
		return getZoomLevel (pix_in_meters, null, null);
	}
	
	public static boolean isZoomValid(double pix_in_meters) {
		boolean [] is_zoom_valid = new boolean[1];
		getZoomLevel(pix_in_meters,is_zoom_valid, null);
		return is_zoom_valid[0];
	}
	
	public static double needZoomIn(double pix_in_meters) {
		double  [] extra_zoom = new double[1];
		getZoomLevel(pix_in_meters,null, extra_zoom);
		return extra_zoom[0];
	}
	
	public double needZoomIn() {
		return needZoomIn(pix_meters);
	}
	
	public int getZoomLevel() {
		return zoom_lev;
	}
	
	/**
	 * Find scale level (0: 1pix/cm, 1 - 2pix/cm, -1 - 0.5 pix/cm) from pixel size in meters
	 * If scale does not match, provide false in optional valid_zoom[0]
	 * zoom_in_extra  
	 * @param pix_size_in_meters pixel size in meters (e.g. 0.02 means 2cm/pix)
	 * @param valid_zoom if provided boolean[1] will return true for valid scale
	 * @param zoom_in_extra enlarge (1.0x..2.0x) (reduce pixel size) to get to the standard scale 
	 * @return zoom level (the higher - the more detailed) 0: - 1cm/pix,-1: 2cm/pix, 1: 0.5 cm/pix 
	 */
	
	public static int getZoomLevel(
			double pix_size_in_meters, // pixel size in meters
			boolean [] valid_zoom,
			double []  zoom_in_extra) {
		int zl = 0;
		double e = 1E-6;
		double pix_size_in_cm = 100 * pix_size_in_meters; // pixel size in cm
		boolean vz = true;
		double ze = 1.0;
		if (pix_size_in_cm > (1.0-e)) { // low resolution, pixel size > 1cm
			zl++;
			while (pix_size_in_cm > (1.0-e)) {
				zl--;		
				pix_size_in_cm /= 2;
			} // exits with (1-e) >= pix_in_cm > 0.5 - e/2
			vz = pix_size_in_cm < (0.5+e);
			if (!vz) {
				ze = 2.0 * pix_size_in_cm;
			}
		} else { // high resolution, pixel size < 1 
//			zl--;
			while (pix_size_in_cm <= (1.0-e)) {
				zl++;		
				pix_size_in_cm *= 2;
			}// exits with (2-2*e) >= pix_in_cm > (1-e)
			vz = pix_size_in_cm < (1.0 + e);
			if (!vz) {
				ze = pix_size_in_cm;
			}
		}
		if (valid_zoom != null) {
			valid_zoom[0] = vz; 
		}
		if (zoom_in_extra != null) {
			zoom_in_extra[0] = ze;
		}
		return zl;
	}
	
	// processing altitudes to remove non-flat surfaces from fitting ortho maps
	
}
