/*-
 * #%L
 * Fiji distribution of ImageJ for the life sciences.
 * %%
 * Copyright (C) 2007 - 2021 Fiji developers.
 * %%
 * This program 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 2 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/gpl-2.0.html>.
 * #L%
 */
package mpicbg.spim.fusion;

import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;

import spim.vecmath.Point3d;
import spim.vecmath.Point3f;

import mpicbg.imglib.cursor.LocalizableByDimCursor;
import mpicbg.imglib.cursor.LocalizableCursor;
import mpicbg.imglib.image.Image;
import mpicbg.imglib.image.ImageFactory;
import mpicbg.imglib.interpolation.Interpolator;
import mpicbg.imglib.interpolation.linear.LinearInterpolatorFactory;
import mpicbg.imglib.multithreading.SimpleMultiThreading;
import mpicbg.imglib.outofbounds.OutOfBoundsStrategyValueFactory;
import mpicbg.imglib.type.numeric.real.FloatType;
import mpicbg.imglib.util.Util;
import mpicbg.models.AbstractAffineModel3D;
import mpicbg.models.NoninvertibleModelException;
import mpicbg.spim.io.IOFunctions;
import mpicbg.spim.registration.ViewDataBeads;
import mpicbg.spim.registration.ViewStructure;

public class MappingFusionParalellMaxWeight extends SPIMImageFusion
{
	final Image<FloatType> fusedImage;

	public MappingFusionParalellMaxWeight( final ViewStructure viewStructure, final ViewStructure referenceViewStructure,
								  final ArrayList<IsolatedPixelWeightenerFactory<?>> isolatedWeightenerFactories,
								  final ArrayList<CombinedPixelWeightenerFactory<?>> combinedWeightenerFactories )
	{
		super( viewStructure, referenceViewStructure, isolatedWeightenerFactories, combinedWeightenerFactories );


		if ( viewStructure.getDebugLevel() <= ViewStructure.DEBUG_MAIN )
			IOFunctions.println("(" + new Date(System.currentTimeMillis()) + "): Reserving memory for fused image.");

		final ImageFactory<FloatType> fusedImageFactory = new ImageFactory<FloatType>( new FloatType(), conf.processImageFactory );
		fusedImage = fusedImageFactory.createImage( new int[]{ imgW, imgH, imgD }, "Fused image");

		if (fusedImage == null)
		{
			if ( viewStructure.getDebugLevel() <= ViewStructure.DEBUG_ERRORONLY )
				IOFunctions.println("MappingFusionParalell.constructor: Cannot create output image: " + conf.processImageFactory.getErrorMessage());

			return;
		}
	}

	@Override
	public void fuseSPIMImages( final int channelIndex )
	{
		if ( viewStructure.getDebugLevel() <= ViewStructure.DEBUG_MAIN )
			IOFunctions.println("Loading source images (Channel " + channelIndex +  ").");

		//
		// update views so that only the current channel is being fused
		//
		final ArrayList<ViewDataBeads> views = new ArrayList<ViewDataBeads>();

		for ( final ViewDataBeads view : viewStructure.getViews() )
			if ( view.getChannelIndex() == channelIndex )
				views.add( view );

		final int numViews = views.size();

		// clear the previous output image
		if ( channelIndex > 0 )
			for ( final FloatType type : fusedImage )
				type.set( 0 );

		// load images
		for ( final ViewDataBeads view : views )
			view.getImage( false );

		if ( viewStructure.getDebugLevel() <= ViewStructure.DEBUG_MAIN && isolatedWeightenerFactories.size() > 0 )
		{
			String methods = "(" + isolatedWeightenerFactories.get(0).getDescriptiveName();
			for ( int i = 1; i < isolatedWeightenerFactories.size(); ++i )
				methods += ", " + isolatedWeightenerFactories.get(i).getDescriptiveName();
			methods += ")";

			IOFunctions.println( "(" + new Date(System.currentTimeMillis()) + "): Init isolated weighteners for all views " + methods );
		}

		// init isolated pixel weighteners
		final AtomicInteger ai = new AtomicInteger(0);
        Thread[] threads = SimpleMultiThreading.newThreads(conf.numberOfThreads);
        final int numThreads = threads.length;

        // compute them all in paralell ( computation done while opening )
		IsolatedPixelWeightener<?>[][] isoWinit = new IsolatedPixelWeightener<?>[ isolatedWeightenerFactories.size() ][ numViews ];
		for (int j = 0; j < isoWinit.length; j++)
		{
			final int i = j;

			final IsolatedPixelWeightener<?>[][] isoW = isoWinit;

			for (int ithread = 0; ithread < threads.length; ++ithread)
	            threads[ithread] = new Thread(new Runnable()
	            {
	                @Override
					public void run()
	                {
	                	final int myNumber = ai.getAndIncrement();

						for (int view = 0; view < numViews; view++)
							if ( view % numThreads == myNumber)
							{
								IOFunctions.println( "Computing " + isolatedWeightenerFactories.get( i ).getDescriptiveName() + " for " + views.get( view ) );
								isoW[i][view] = isolatedWeightenerFactories.get(i).createInstance( views.get(view) );
							}
	                }
	            });

			SimpleMultiThreading.startAndJoin( threads );
		}

		// test if the isolated weighteners were successfull...
		try
		{
			boolean successful = true;
			for ( final IsolatedPixelWeightener[] iso : isoWinit )
				for ( final IsolatedPixelWeightener i : iso )
					if ( i == null )
						successful = false;

			if ( !successful )
			{
				IOFunctions.println( "Not enough memory for running the content-based fusion, running without it" );
				isoWinit = new IsolatedPixelWeightener[ 0 ][ 0 ];
			}
		}
		catch (final Exception e)
		{
			IOFunctions.println( "Not enough memory for running the content-based fusion, running without it" );
			isoWinit = new IsolatedPixelWeightener[ 0 ][ 0 ];
		}

		final IsolatedPixelWeightener<?>[][] isoW = isoWinit;

		if ( viewStructure.getDebugLevel() <= ViewStructure.DEBUG_MAIN )
			IOFunctions.println( "(" + new Date(System.currentTimeMillis()) + "): Computing output image (Channel " + channelIndex +  ").");

		// cache the views, imageSizes and models that we use
		final boolean useView[] = new boolean[ numViews ];
		final AbstractAffineModel3D<?> models[] = new AbstractAffineModel3D[ numViews ];

		for ( int i = 0; i < numViews; ++i )
		{
			useView[ i ] = Math.max( views.get( i ).getViewErrorStatistics().getNumConnectedViews(), views.get( i ).getTile().getConnectedTiles().size() ) > 0 || views.get( i ).getViewStructure().getNumViews() == 1;

			// if a corresponding view that was used for registration is valid, this one is too
			if ( views.get( i ).getUseForRegistration() == false )
			{
				final int angle = views.get( i ).getAcqusitionAngle();
				final int timepoint = views.get( i ).getViewStructure().getTimePoint();

				for ( final ViewDataBeads view2 : viewStructure.getViews() )
					if ( view2.getAcqusitionAngle() == angle && timepoint == view2.getViewStructure().getTimePoint() && view2.getUseForRegistration() == true )
						useView[ i ] = true;
			}

			models[ i ] = (AbstractAffineModel3D<?>)views.get( i ).getTile().getModel();
		}

		// we want the image to be not normalized to [0...1]
		for (int view = 0; view < numViews ; view++)
			views.get( view ).unnormalizeImage();

		final int[][] imageSizes = new int[numViews][];
		for ( int i = 0; i < numViews; ++i )
			imageSizes[ i ] = views.get( i ).getImageSize();

		ai.set( 0 );
        threads = SimpleMultiThreading.newThreads( numThreads );

		for (int ithread = 0; ithread < threads.length; ++ithread)
            threads[ithread] = new Thread(new Runnable()
            {
                @Override
				public void run()
                {
                	try
                	{
                        final int myNumber = ai.getAndIncrement();

                        // temporary float array
		            	final double[] tmp = new double[ 3 ];

		        		// init combined pixel weighteners
		        		if ( viewStructure.getDebugLevel() <= ViewStructure.DEBUG_MAIN && combinedWeightenerFactories.size() > 0 )
		        		{
		        			String methods = "(" + combinedWeightenerFactories.get(0).getDescriptiveName();
		        			for ( int i = 1; i < combinedWeightenerFactories.size(); ++i )
		        				methods += ", " + combinedWeightenerFactories.get(i).getDescriptiveName();
		        			methods += ")";

		        			if ( myNumber == 0 )
		        				IOFunctions.println("Initialize combined weighteners for all views " + methods );
		        		}

		        		final CombinedPixelWeightener<?>[] combW = new CombinedPixelWeightener<?>[combinedWeightenerFactories.size()];
		        		for (int i = 0; i < combW.length; i++)
		        			combW[i] = combinedWeightenerFactories.get(i).createInstance( views );

						// get iterators for isolated weights
		        		final LocalizableByDimCursor<FloatType> isoIterators[][] = new LocalizableByDimCursor[ isoW.length ][ numViews ];
						for (int i = 0; i < isoW.length; i++)
							for (int view = 0; view < isoW[i].length; view++)
								isoIterators[i][view] = isoW[i][view].getResultIterator();

		    			final Point3d[] tmpCoordinates = new Point3d[ numViews ];
		    			final int[][] loc = new int[ numViews ][ 3 ];
		    			final double[][] locd = new double[ numViews ][ 3 ];
		    			final boolean[] use = new boolean[ numViews ];

		    			for ( int i = 0; i < numViews; ++i )
		    				tmpCoordinates[ i ] = new Point3d();

		    			final LocalizableCursor<FloatType> it = fusedImage.createLocalizableCursor();

		    			// create Interpolated Iterators for the input images (every thread need own ones!)
		    			final Interpolator<FloatType>[] imageInterpolators = new Interpolator[ numViews ];
		    			for (int view = 0; view < numViews ; view++)
		    				imageInterpolators[ view ] = views.get( view ).getImage( false ).createInterpolator( conf.interpolatorFactorOutput );

		    			// create Interpolators for weights
		    			final Interpolator<FloatType>[][] weightInterpolators = new Interpolator[ isoW.length ][ numViews ];
		    			for (int i = 0; i < isoW.length; i++)
		    				for (int view = 0; view < numViews ; view++)
		    					weightInterpolators[i][ view ] = isoW[i][view].getResultImage().createInterpolator( new LinearInterpolatorFactory<FloatType>( new OutOfBoundsStrategyValueFactory<FloatType>()) );

		    			while (it.hasNext())
		    			{
		    				it.fwd();

		        			if (it.getPosition(2) % numThreads == myNumber)
		        			{
		        				// get the coordinates if cropped
		        				final int x = it.getPosition(0) + cropOffsetX;
		        				final int y = it.getPosition(1) + cropOffsetY;
		        				final int z = it.getPosition(2) + cropOffsetZ;

								int num = 0;
								for (int i = 0; i < numViews; ++i)
								{
									if ( useView[ i ] )
									{
		    							tmpCoordinates[ i ].x = x * scale + min.x;
		    							tmpCoordinates[ i ].y = y * scale + min.y;
		    							tmpCoordinates[ i ].z = z * scale + min.z;

		    							mpicbg.spim.mpicbg.Java3d.applyInverseInPlace( models[i], tmpCoordinates[i], tmp );

		    							loc[i][0] = (int)Util.round( tmpCoordinates[i].x );
		    							loc[i][1] = (int)Util.round( tmpCoordinates[i].y );
		    							loc[i][2] = (int)Util.round( tmpCoordinates[i].z );

		    							locd[i][0] = tmpCoordinates[i].x;
		    							locd[i][1] = tmpCoordinates[i].y;
		    							locd[i][2] = tmpCoordinates[i].z;

		    							// do we hit the source image?
										if ( loc[ i ][ 0 ] >= 0 && loc[ i ][ 1 ] >= 0 && loc[ i ][ 2 ] >= 0 &&
											 loc[ i ][ 0 ] < imageSizes[ i ][ 0 ] &&
											 loc[ i ][ 1 ] < imageSizes[ i ][ 1 ] &&
											 loc[ i ][ 2 ] < imageSizes[ i ][ 2 ] )
		    							{
		    								use[i] = true;
		    								++num;
		    							}
		    							else
		    							{
		    								use[i] = false;
		    							}
									}
								}

								if (num > 0)
								{
		    						// update combined weighteners
									if (combW.length > 0)
										for (final CombinedPixelWeightener<?> w : combW)
											w.updateWeights(locd, use);

									//float sumWeights = 0;
		    						//float value = 0;

    								int maxView = -1;
    								float maxWeight = -1;

		    						for (int view = 0; view < numViews; ++view)
		    							if (use[view])
		    							{
		    								float weight = 1;

		    								// multiplicate combined weights
		    								if (combW.length > 0)
		    									for (final CombinedPixelWeightener<?> w : combW)
		    										weight *= w.getWeight(view);

		    								tmp[ 0 ] = tmpCoordinates[view].x;
		    								tmp[ 1 ] = tmpCoordinates[view].y;
		    								tmp[ 2 ] = tmpCoordinates[view].z;

		    								for (int i = 0; i < isoW.length; i++)
		    								{
		    									weightInterpolators[i][view].setPosition( tmp );
		    									weight = weightInterpolators[i][view].getType().get();
		    								}

		    								if ( weight > maxWeight )
		    								{
		    									maxWeight = weight;
		    									maxView = view;
		    								}

		    								/*
		    								tmp[ 0 ] = tmpCoordinates[view].x;
		    								tmp[ 1 ] = tmpCoordinates[view].y;
		    								tmp[ 2 ] = tmpCoordinates[view].z;

		    								interpolators[view].moveTo( tmp );

		    								value += weight * interpolators[view].getType().get();
		    								sumWeights += weight;
		    								*/
		    							}

		    						if ( maxView != -1 )
		    						{
		    							/*
		    							// set label
		    							it.getType().set( views.get( maxView ).getAcqusitionAngle() + 10 );
		    							*/

		    							// set intensity of the maxview
		    							tmp[ 0 ] = tmpCoordinates[maxView].x;
	    								tmp[ 1 ] = tmpCoordinates[maxView].y;
	    								tmp[ 2 ] = tmpCoordinates[maxView].z;

	    								imageInterpolators[maxView].setPosition( tmp );
		    							it.getType().set( imageInterpolators[maxView].getType().get() );
		    						}

	    						}

	            			} // myThread loop
	        			} // iterator loop

	        			it.close();
	        			for (int i = 0; i < isoW.length; i++)
							for (int view = 0; view < numViews; view++)
								weightInterpolators[i][view].close();

						for (int view = 0; view < numViews; view++)
							imageInterpolators[view].close();

	        			// close combined pixel weighteners
	        			for (int i = 0; i < combW.length; i++)
	        				combW[i].close();

	        			// close isolated iterators
						for (int i = 0; i < isoW.length; i++)
							for (int view = 0; view < isoW[i].length; view++)
								isoIterators[i][view].close();

                	}
                	catch (final NoninvertibleModelException e)
                	{
                		if ( viewStructure.getDebugLevel() <= ViewStructure.DEBUG_ERRORONLY )
                			IOFunctions.println( "MappingFusionParalell(): Model not invertible for " + viewStructure );
                	}
                }// Thread.run loop
            });

        SimpleMultiThreading.startAndJoin(threads);

		if ( viewStructure.getDebugLevel() <= ViewStructure.DEBUG_MAIN )
			IOFunctions.println("(" + new Date(System.currentTimeMillis()) + "): Closing all input images (Channel " + channelIndex +  ").");

		// unload images
		for ( final ViewDataBeads view : views )
			view.closeImage();

		// close weighteners
		// close isolated pixel weighteners
		try
		{
			for (int i = 0; i < isoW.length; i++)
				for (int view = 0; view < numViews; view++)
					isoW[i][view].close();
		}
		catch (final Exception e )
		{
			// this will fail if there was not enough memory...
		}

		if ( viewStructure.getDebugLevel() <= ViewStructure.DEBUG_MAIN )
			IOFunctions.println("(" + new Date(System.currentTimeMillis()) + "): Done computing output image (Channel " + channelIndex +  ").");
	}

	@Override
	public Image<FloatType> getFusedImage() { return fusedImage; }
}
