/**
 **
 ** CuasData.java - cuas data
 **
 ** Copyright (C) 2025 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  CuasData.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.cuas;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.QuadCLT;
import com.elphel.imagej.tileprocessor.QuadCLTCPU;

import ij.Prefs;

public class CuasData implements Serializable {
	private static final long  serialVersionUID = 1L;
	public static final int    VERSION_INITIAL =  100; // to be able to read older version and save later one
	public static int          LATEST_VERSION =   VERSION_INITIAL;  // 100; // use when read from .list
	public static int          CURRENT_VERSION =  LATEST_VERSION; 
	public static final String CUAS_EXTENSION =   ".cuas";
	public static final String CUAS_CUMULATIVE =  "-CUMUL";
	public final ArrayList<ArrayList<CuasTile>> data;
	public final int           num_colors;
	public final int           width; // in tiles
	public final int           height;// in tiles
	public String              filePath = null;
	
	public static String getCuasSuffix() {
		return "-V"+CURRENT_VERSION+CUAS_EXTENSION;
	}
	public static String getCuasCumulativeSuffix() {
		return CUAS_CUMULATIVE+"-V"+CURRENT_VERSION+CUAS_EXTENSION;
	}
	
	public void  setFilePath(String parent_path) {
		this.filePath = parent_path;
	}

	public String getFilePath() {
		return filePath;
	}

	public static CuasData getCuasData(
			String  full_path,  // ends with /vxx
			int     num_colors, // only used if restored from tiff, ignored for .cuas
			int     width,      // only used if restored from tiff, ignored for .cuas
			double  dts,        // time stamp as double, only used if restored from tiff, ignored for .cuas
			int     debugLevel) {
		return getCuasData(
				true, // boolean try_cumul,
				full_path,   // String  full_path,  // ends with /vxx
				num_colors,  // int     num_colors, // only used if restored from tiff, ignored for .cuas
				width,       // int     width,      // only used if restored from tiff, ignored for .cuas
				dts,         // double  dts,        // time stamp as double, only used if restored from tiff, ignored for .cuas
				debugLevel); // int     debugLevel)
	}
	public static CuasData getCuasData(
			boolean try_cumul,  // and save as such if only old style existed
			String  full_path,  // ends with /vxx
			int     num_colors, // only used if restored from tiff, ignored for .cuas
			int     width,      // only used if restored from tiff, ignored for .cuas
			double  dts,        // time stamp as double, only used if restored from tiff, ignored for .cuas
			int     debugLevel) {
		if (full_path == null) {
			return null; 
		}
		while (full_path.endsWith(Prefs.getFileSeparator())) { // remove trailing "/"
			full_path = full_path.substring(0,full_path.length()-1);
		}
		int ver_sep = full_path.lastIndexOf(Prefs.getFileSeparator());
		int name_sep = full_path.lastIndexOf(Prefs.getFileSeparator(),ver_sep-1);
		String name = full_path.substring(name_sep+1,ver_sep);
		File cdir_parent = new File(full_path);
		if (!cdir_parent.exists() || !cdir_parent.isDirectory()) {
			System.out.println("getCuasData(): directory does not exist: "+full_path);
			return null;
		}
		// try to get data from .cuas file
		CuasData cuasData = null;
		boolean used_cumul = false;
		String cuas_cumul_path = full_path+Prefs.getFileSeparator() + name+ CuasData.getCuasCumulativeSuffix(); // getCuasSuffix();
		String cuas_path =       full_path+Prefs.getFileSeparator() + name+ CuasData.getCuasSuffix();
		if (try_cumul) {
			try {
				cuasData = readCuasData (cuas_cumul_path); //  throws IOException, ClassNotFoundException {
				if (debugLevel >-3) {
					System.out.println("getCuasData(): read cumulative CuasData from "+cuas_cumul_path+" .");
				}
				used_cumul = true;
			} catch (Exception e) {
				if (debugLevel >-3) {
					System.out.println("getCuasData(): cumulative "+cuas_cumul_path+" does not exist, using single-sequence data instead.");
				}
			}
		}
		if (cuasData == null) {
			cuas_path = full_path+Prefs.getFileSeparator() + name+ CuasData.getCuasSuffix();
			try {
				cuasData = readCuasData (cuas_path); //  throws IOException, ClassNotFoundException {
				if (debugLevel >-3) {
					System.out.println("getCuasData(): read single-scene CuasData from "+cuas_path+" .");
				}
				used_cumul = false;
			} catch (Exception e) {
				if (debugLevel >-3) {
					System.out.println("getCuasData(): "+cuas_path+" does not exist, using image-based (old style) data instead.");
				}
			}
		}
		
		if (cuasData == null) {
			if ((num_colors <= 0) || (width <= 0) || (dts <= 0)) {
				System.out.println("getCuasData(): .cuas not available and num_colors<=0 or width<=0 or dts<=0 - bailing out.");
			}
			int [] wh = new int[2];
			float [][] fclt_w;
			String cuas_old_path = full_path+Prefs.getFileSeparator() + name+ QuadCLTCPU.CENTER_CLT_SUFFIX+ ".tiff";
			fclt_w = ShowDoubleFloatArrays.readFloatArray(
					cuas_old_path, // String      file_path,
					0, // int         num_slices, // (0 - all)
					wh); // int []      wh) {
			if (fclt_w == null) {
				System.out.println("getCuasData(): No FCLT data in "+cuas_old_path);
				return null;
			}
			if (fclt_w.length != 1) {
				System.out.println("getCuasData(): expected a single-slice data, got "+fclt_w.length+" slices.");
				return null;
			}
			final int tile_size =   num_colors * CuasTile.CLT_TILE_LENGTH;
			float [] fclt_weights = new float [fclt_w[0].length/(tile_size + 1)];
			float [] fclt =         new float [fclt_w[0].length/(tile_size + 1) * tile_size];
			System.arraycopy(fclt_w[0], 0,           fclt,         0, fclt.length);
			System.arraycopy(fclt_w[0], fclt.length, fclt_weights, 0, fclt_weights.length);
			cuasData =  new CuasData (
					num_colors,   // final int      num_colors,
					width,        // final int      width, // should be multiple of width
					fclt,         // final float [] fclt,
					fclt_weights, // final float [] fclt_weights, // weights,weights[1] - common weight, null - common weight== 1,
					dts);         // final double   dts) {
			// save .cuas to the same directory
			//cuas_path
			String save_path = try_cumul ? cuas_cumul_path : cuas_path;
			try {
				cuasData.writeCuasData(save_path);
				if (debugLevel > -3) {
					System.out.println("getCuasData(): saved CUAS data to "+save_path);
				}
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return cuasData;
	}
	
	
	
	public CuasData (
			int num_colors,
			int width, // should be multiple of width
			int height) {
		this.num_colors = num_colors;
		this.width = width;
		this.height = height;
		int data_length = width*height;
		data = new ArrayList<ArrayList<CuasTile>>(data_length);
		for (int i = 0; i < data_length; i++) {
			data.add(new ArrayList<CuasTile>(1));
		}
	}
	
	public CuasData (CuasData templateData) {
		this.num_colors = templateData.num_colors;
		this.width = templateData.width;
		this.height = templateData.height;
		int data_length = width*height;
		data = new ArrayList<ArrayList<CuasTile>>(data_length);
		for (int i = 0; i < data_length; i++) {
			data.add(new ArrayList<CuasTile>(templateData.getTileData(i).size()));
		}
		
	}
	
	
	public int getNumTiles() {
		return data.size();
	}

	public int getTilesX() {
		return width;
	}

	public int getTilesY() {
		return height;
	}

	
	public CuasData (
			final int      num_colors,
			final int      width, // should be multiple of width
			final float [] fclt,
			final float [] fclt_weights, // weights,weights[1] - common weight, null - common weight== 1,
			final double   dts) {
		this.num_colors = num_colors;
		this.width = width;
		final int tile_size = num_colors * CuasTile.CLT_TILE_LENGTH; 
		final int data_length = fclt.length/tile_size;
		final double weight = (fclt_weights == null) ? 1.0: ((fclt_weights.length > 1)? Double.NaN: fclt_weights[0]);  
		this.height = data_length/width; 
		data = new ArrayList<ArrayList<CuasTile>>(data_length);
		for (int i = 0; i < data_length; i++) {
			data.add(new ArrayList<CuasTile>(1));
		}
		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 nTile = ai.getAndIncrement(); nTile < data_length; nTile = ai.getAndIncrement()) {
						int offs = nTile * tile_size;
						boolean has_nan = false;
						for (int i = 0; i < tile_size; i++) {
							if (Float.isNaN(fclt[offs+i])) {
								has_nan = true;
								break;
							}
						}
						if (!has_nan) {
							double w = Double.isNaN(weight)? fclt_weights[nTile] : weight;
							CuasTile ctile= new CuasTile(
									num_colors, // int num_colors,
									dts,        // double dts,
									w,          // double weight,
									fclt,       // float [] clt_data);
									nTile);     // 
							data.get(nTile).add(ctile);
						}
	                }
	            }
	        };
	    }		      
	    ImageDtt.startAndJoin(threads);
	    return;
	}
	
	
	
	public ArrayList<CuasTile> getTileData(int ntile){
		return data.get(ntile);
	}

	public CuasTile getTileData(int ntile, int nvar) {
		return data.get(ntile).get(nvar);
	}

	public void sortTile(int ntile) {
		Collections.sort(getTileData(ntile));
	}

	public void addTile(CuasTile tile, int ntile) {
		getTileData(ntile).add(tile);
	}
	
	public float [][] collapse(
			final double tolerance, // NaN works as infinity
			final double decay,
			final double ts_now){
	return collapse(
			tolerance, // final double  tolerance, // NaN works as infinity
			decay,     // final double  decay,
			ts_now,    // final double  ts_now,
			null,      // CLTParameters clt_parameters,
			null);     // QuadCLT       center_CLT)
	}	
	
	public float [][] collapse(
			final double  tolerance, // NaN works as infinity
			final double  decay,
			final double  ts_now,
			CLTParameters clt_parameters,
			QuadCLT       center_CLT){ // if non-null - show image using instance data
		final int dbg_tile = 2029;
		final int tile_size = num_colors * CuasTile.CLT_TILE_LENGTH; 
		final float [] fclt =    new float [data.size() * tile_size];
		final float [] fclt_weights = new float [data.size()];
		final boolean single_variant = tolerance ==0;
		final boolean all_variants = Double.isNaN(tolerance) || Double.isInfinite(tolerance);
		final double tolerance2 = tolerance*tolerance;
		Arrays.fill(fclt, Float.NaN);
		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 nTile = ai.getAndIncrement(); nTile < data.size(); nTile = ai.getAndIncrement()) {
						if (nTile == dbg_tile) {
							System.out.println("collapse(): nTile="+nTile);
						}
						ArrayList<CuasTile> tile_list = getTileData(nTile);
						if (!tile_list.isEmpty()) {
							int num_vars = tile_list.size(); 
							// find best variant
							double [] weights = new double [num_vars];
							int indx_best = 0;
							for (int i = 0;i < num_vars; i++) {
								CuasTile ctile = tile_list.get(i);
								weights[i] = ctile.getWeight(
										decay,     // double decay,
										ts_now);   // double ts_now)
								if (weights[i] > weights[indx_best]) {
									indx_best = i;
								}
							}
							if (tile_list.get(indx_best).clt_data != null) {
								CuasTile tile = tile_list.get(indx_best).clone();
								if ((num_vars > 1) && !single_variant){
									for (int i = 0; i < num_vars; i++) if ((i != indx_best) && !tile_list.get(i).isEmpty()) {
										if (all_variants || (tile_list.get(indx_best).diffTile2(tile_list.get(i)) <= tolerance2)) {
											tile.merge(tile_list.get(i),decay);
										}									
									}
								}
								tile.getData (
										fclt,   // float [] fclt
										nTile); // int      ntile)
								fclt_weights[nTile] = (float) tile.getWeight(decay, ts_now);
							} else {
								System.out.println("collapse(): tile_list.get("+indx_best+")==null, num_vars= "+num_vars+", nTile="+nTile+", dts="+tile_list.get(indx_best).dts+", weight="+tile_list.get(indx_best).weight);
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		if ((center_CLT != null) && (clt_parameters != null)){
			center_CLT.showCenterClt(
    				new float [][] {fclt}, // float [][] fclt, // may be null (do not save to center_CLT instance, just show/save)
    				clt_parameters,        // CLTParameters clt_parameters,
    				true);                 // boolean       show);
        	ShowDoubleFloatArrays.showArrays(
        			fclt_weights,
        			center_CLT.getTilesX(),
        			center_CLT.getTilesY(),
        			"collapse_weights");
		}
		return new float [][] {fclt,fclt_weights};
	}
	
	public CuasData cloneEmpty() {
		CuasData cuasData = new CuasData(num_colors, width, height);
		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 nTile = ai.getAndIncrement(); nTile < data.size(); nTile = ai.getAndIncrement()) {
						ArrayList<CuasTile> tile_list = getTileData(nTile);
						if (!tile_list.isEmpty()) {
							ArrayList<CuasTile> tile_list_new = cuasData.getTileData(nTile);
							for (CuasTile tile:tile_list) {
								tile_list_new.add(tile.cloneEmpty());
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return cuasData;
	}

	
	
	@Override
	public CuasData clone() {
		CuasData cuasData = new CuasData(num_colors, width, height);
		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 nTile = ai.getAndIncrement(); nTile < data.size(); nTile = ai.getAndIncrement()) {
						ArrayList<CuasTile> tile_list = getTileData(nTile);
						if (!tile_list.isEmpty()) {
							ArrayList<CuasTile> tile_list_new = cuasData.getTileData(nTile);
							for (CuasTile tile:tile_list) {
								tile_list_new.add(tile.clone());
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return cuasData;
	}
	
	@SuppressWarnings("static-method")
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
		ois.defaultReadObject();
	}

	@SuppressWarnings("static-method")
	private void writeObject(ObjectOutputStream oos) throws IOException {
		oos.defaultWriteObject();
	}
	
	public void writeCuasData (String path) throws IOException {
		setFilePath(path);
		FileOutputStream fileOutputStream = new FileOutputStream(path);
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
 		objectOutputStream.writeObject(LATEST_VERSION); // current_version);
		objectOutputStream.writeObject(this);
	    objectOutputStream.flush();
	    objectOutputStream.close();
	}
	
	public static CuasData readCuasData (
			String path) throws IOException, ClassNotFoundException {
		// try reading current_version, if fails - restart without it (for older formats)
		FileInputStream fileInputStream  = new FileInputStream(path);
		ObjectInputStream objectInputStream  = new ObjectInputStream(fileInputStream);
		int current_version = (int) objectInputStream.readObject(); // reads OrthoMapsCollection
		CuasData.CURRENT_VERSION = current_version; // trying here 08.29.2024 - before orthoMapsCollection exists
///		System.out.println("readCuasData(): got current_version="+current_version);
		CuasData cuasData = (CuasData) objectInputStream.readObject();
		objectInputStream.close();
///		System.out.println("readCuasData(): got orthoMapsCollection, current_version="+current_version);
		return cuasData;
	}
	
	

}
