/*
 * #%L
 * ImgLib2: a general-purpose, multidimensional image processing library.
 * %%
 * Copyright (C) 2009 - 2016 Tobias Pietzsch, Stephan Preibisch, Stephan Saalfeld,
 * John Bogovic, Albert Cardona, Barry DeZonia, Christian Dietz, Jan Funke,
 * Aivar Grislis, Jonathan Hale, Grant Harris, Stefan Helfrich, Mark Hiner,
 * Martin Horn, Steffen Jaensch, Lee Kamentsky, Larry Lindsey, Melissa Linkert,
 * Mark Longair, Brian Northan, Nick Perry, Curtis Rueden, Johannes Schindelin,
 * Jean-Yves Tinevez and Michael Zinsmaier.
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */

package net.imglib2.view;

import java.util.LinkedList;
import java.util.ListIterator;

import net.imglib2.Interval;
import net.imglib2.RandomAccess;
import net.imglib2.RandomAccessible;
import net.imglib2.RandomAccessibleInterval;
import net.imglib2.img.WrappedImg;
import net.imglib2.transform.Transform;
import net.imglib2.transform.integer.BoundingBox;
import net.imglib2.transform.integer.BoundingBoxTransform;
import net.imglib2.transform.integer.Mixed;
import net.imglib2.transform.integer.MixedTransform;
import net.imglib2.transform.integer.SlicingTransform;
import net.imglib2.transform.integer.TranslationTransform;
import net.imglib2.util.Intervals;

/**
 * The "brain" of the Views framework. Simplifies View cascades to provide the
 * most efficient accessor for a specified Interval.
 * 
 * @see #getEfficientRandomAccessible(Interval, RandomAccessible)
 * 
 * @author Tobias Pietzsch
 */
public class TransformBuilder< T >
{
	/**
	 * Get a RandomAccessible which provides RandomAccess to the specified
	 * {@code interval} of {@code randomAccessible}.
	 * 
	 * <p>
	 * Create a new TransformBuilder that traverses the view hierarchy starting
	 * from {@code randomAccessible}. {@link #build()} an efficient
	 * RandomAccessible by joining and simplifying the collected
	 * transformations.
	 * </p>
	 * 
	 * @param interval
	 *            The interval in which access is needed.
	 * @param randomAccessible
	 */
	public static < S > RandomAccessible< S > getEfficientRandomAccessible( final Interval interval, final RandomAccessible< S > randomAccessible )
	{
		return new TransformBuilder< S >( interval, randomAccessible ).build();
	}

	/**
	 * Provides the untransformed random access.
	 */
	protected RandomAccessible< T > source;

	/**
	 * Interval transformed to the currently visited view. null means that the
	 * interval is infinite.
	 */
	protected BoundingBox boundingBox;

	/**
	 * List of transforms that have to be applied when wrapping the
	 * {@link #source} RandomAccess to obtain a RandomAccess in the target
	 * coordinate system.
	 */
	protected LinkedList< Transform > transforms;

	/**
	 * Create a new TransformBuilder. Starting from {@code randomAccessible}, go
	 * down the view hierarchy to the RandomAccessible that will provide the
	 * source RandomAccess into the specified {@code interval}. While traversing
	 * the view hierarchy transforms are collected into the {@link #transforms}
	 * list. These transforms have to be applied when wrapping the source
	 * RandomAccess to obtain a RandomAccess in the coordinate system of
	 * {@code randomAccessible}.
	 * 
	 * @param interval
	 *            The interval in which access is needed. This is converted to a
	 *            bounding box which is propagated through the transforms down
	 *            the view hierarchy.
	 * @param randomAccessible
	 */
	protected TransformBuilder( final Interval interval, final RandomAccessible< T > randomAccessible )
	{
		transforms = new LinkedList< Transform >();
		boundingBox = ( interval == null ) ? null : new BoundingBox( interval );
		// System.out.println( randomAccessible );
		visit( randomAccessible );
		simplifyTransforms();
	}

	/**
	 * Prepend a transform to the {@link #transforms} list. Also apply the
	 * transform to {@link #boundingBox}, which will be used to specify the
	 * interval for the RandomAccess on the final source (at the end of the view
	 * chain). This is called while traversing the view hierarchy.
	 * 
	 * @param t
	 *            the transform to add.
	 */
	protected void prependTransform( final Transform t )
	{
		if ( BoundingBoxTransform.class.isInstance( t ) && ( boundingBox != null ) )
			boundingBox = ( ( BoundingBoxTransform ) t ).transform( boundingBox );
		else
			boundingBox = null;
		transforms.addFirst( t );
	}

	/**
	 * Visit a RandomAccessible (while traversing the view hierarchy). The
	 * {@code randomAccessible} is handled by
	 * {@link #visitTransformed(TransformedRandomAccessible)} or
	 * {@link #visitExtended(ExtendedRandomAccessibleInterval)} when it has the
	 * appropriate type. Otherwise, the traversal stops and
	 * {@code randomAccessible} is set as the {@link #source}.
	 * 
	 * @param randomAccessible
	 */
	@SuppressWarnings( "unchecked" )
	protected void visit( final RandomAccessible< T > randomAccessible )
	{
		if ( TransformedRandomAccessible.class.isInstance( randomAccessible ) )
		{
			visitTransformed( ( TransformedRandomAccessible< T > ) randomAccessible );
		}
		else if ( ExtendedRandomAccessibleInterval.class.isInstance( randomAccessible ) )
		{
			visitExtended( ( ExtendedRandomAccessibleInterval< T, ? > ) randomAccessible );
		}
		else if ( IntervalView.class.isInstance( randomAccessible ) )
		{
			visit( ( ( IntervalView< T > ) randomAccessible ).getSource() );
		}
		else if ( WrappedImg.class.isInstance( randomAccessible ) )
		{
			visit( ( ( WrappedImg< T > ) randomAccessible ).getImg() );
		}
		else
		{
			source = randomAccessible;
		}
	}

	/**
	 * Visit a TransformedRandomAccessible (while traversing the view
	 * hierarchy). Append the view's transform to the list and
	 * {@link #visit(RandomAccessible)} the view's source.
	 * 
	 * @param randomAccessible
	 */
	protected void visitTransformed( final TransformedRandomAccessible< T > randomAccessible )
	{
		prependTransform( randomAccessible.getTransformToSource() );
		visit( randomAccessible.getSource() );
	}

	/**
	 * Visit a ExtendedRandomAccessibleInterval (while traversing the view
	 * hierarchy). If the no out-of-bounds extension is needed for the current
	 * bounding box, {@link #visit(RandomAccessible)} the view's source.
	 * Otherwise, the traversal stops and {@code randomAccessible} is set as the
	 * {@link #source}.
	 * 
	 * @param randomAccessible
	 */
	protected void visitExtended( final ExtendedRandomAccessibleInterval< T, ? > randomAccessible )
	{
		final RandomAccessibleInterval< T > sourceInterval = randomAccessible.getSource();
		if ( ( boundingBox != null ) && Intervals.contains( sourceInterval, boundingBox.getInterval() ) )
			visit( sourceInterval );
		else
			source = randomAccessible;
	}

	public static boolean isIdentity( final Mixed t )
	{
		final int n = t.numSourceDimensions();
		final int m = t.numTargetDimensions();
		if ( n != m )
			return false;

		for ( int d = 0; d < m; ++d )
		{
			if ( t.getTranslation( d ) != 0 )
				return false;
			if ( t.getComponentZero( d ) )
				return false;
			if ( t.getComponentInversion( d ) )
				return false;
			if ( t.getComponentMapping( d ) != d )
				return false;
		}
		return true;
	}

	public static boolean isTranslation( final Mixed t )
	{
		final int n = t.numSourceDimensions();
		final int m = t.numTargetDimensions();
		if ( n != m )
			return false;

		for ( int d = 0; d < m; ++d )
		{
			if ( t.getComponentZero( d ) )
				return false;
			if ( t.getComponentInversion( d ) )
				return false;
			if ( t.getComponentMapping( d ) != d )
				return false;
		}
		return true;
	}

	public static boolean isComponentMapping( final Mixed t )
	{
		final int m = t.numTargetDimensions();

		for ( int d = 0; d < m; ++d )
		{
			if ( t.getTranslation( d ) != 0 )
				return false;
			if ( t.getComponentZero( d ) )
				return false;
			if ( t.getComponentInversion( d ) )
				return false;
		}
		return true;
	}

	public static boolean isSlicing( final Mixed t )
	{
		final int n = t.numSourceDimensions();
		final int m = t.numTargetDimensions();
		if ( n > m )
			return false;

		for ( int d = 0; d < m; ++d )
		{
			if ( t.getTranslation( d ) != 0 && ( !t.getComponentZero( d ) ) )
				return false;
			if ( t.getComponentInversion( d ) )
				return false;
		}
		return true;
	}

	/**
	 * Simplify the {@link #transforms} list. First, concatenate neighboring
	 * transforms if possible. Then, for every {@link Mixed} transform:
	 * <ul>
	 * <li>remove it if it is the identity transforms.
	 * <li>replace it by a {@link TranslationTransform} if it is a pure
	 * translation.
	 * <li>replace it by a {@link SlicingTransform} if it is a pure slicing.
	 * </ul>
	 */
	protected void simplifyTransforms()
	{
		net.imglib2.concatenate.ConcatenateUtils.join( transforms );

		for ( final ListIterator< Transform > i = transforms.listIterator(); i.hasNext(); )
		{
			final Transform t = i.next();
			if ( Mixed.class.isInstance( t ) )
			{
				final Mixed mixed = ( Mixed ) t;
				if ( isIdentity( mixed ) )
				{
					// found identity
					// remove from transforms list
					i.remove();
				}
				else if ( isTranslation( mixed ) )
				{
					// found pure translation
					// replace by a TranslationTransform
					final long[] translation = new long[ mixed.numTargetDimensions() ];
					mixed.getTranslation( translation );
					i.set( new TranslationTransform( translation ) );
				}
				// else if ( isComponentMapping( mixed ) )
//				{
//					// found pure component mapping
//					// replace by a ComponentMappingTransform
//					final int[] component = new int[ mixed.numTargetDimensions() ];
//					mixed.getComponentMapping( component );
//					i.set( new ComponentMappingTransform( component ) );
//				}
				else if ( isSlicing( mixed ) )
				{
					// found pure slicing
					// replace by a SlicingTransform
					final int m = mixed.numTargetDimensions();
					final long[] translation = new long[ m ];
					final boolean[] zero = new boolean[ m ];
					final int[] component = new int[ m ];
					mixed.getTranslation( translation );
					mixed.getComponentZero( zero );
					mixed.getComponentMapping( component );
					final SlicingTransform sl = new SlicingTransform( mixed.numSourceDimensions(), m );
					sl.setTranslation( translation );
					sl.setComponentZero( zero );
					sl.setComponentMapping( component );
					i.set( sl );
				}
			}
		}
	}

	/**
	 * Create a sequence of wrapped RandomAccessibles from the
	 * {@link #transforms} list.
	 * 
	 * @return RandomAccessible on the interval specified in the constructor.
	 */
	protected RandomAccessible< T > build()
	{
		RandomAccessible< T > result = source;
		for ( final ListIterator< Transform > i = transforms.listIterator(); i.hasNext(); )
		{
			final Transform t = i.next();
			if ( MixedTransform.class.isInstance( t ) )
				result = wrapMixedTransform( result, ( MixedTransform ) t );
			else if ( TranslationTransform.class.isInstance( t ) )
				result = wrapTranslationTransform( result, ( TranslationTransform ) t );
			else if ( SlicingTransform.class.isInstance( t ) )
				result = wrapSlicingTransform( result, ( SlicingTransform ) t );
			else
				result = wrapGenericTransform( result, t );
		}
		return result;
	}

	protected RandomAccessible< T > wrapGenericTransform( final RandomAccessible< T > s, final Transform t )
	{
		return new RandomAccessible< T >()
		{
			@Override
			public int numDimensions()
			{
				return t.numSourceDimensions();
			}

			@Override
			public RandomAccess< T > randomAccess()
			{
				return new TransformRandomAccess< T >( s.randomAccess(), t );
			}

			@Override
			public RandomAccess< T > randomAccess( final Interval interval )
			{
				return new TransformRandomAccess< T >( s.randomAccess(), t );
			}
		};
	}

	protected RandomAccessible< T > wrapMixedTransform( final RandomAccessible< T > s, final MixedTransform t )
	{
		final boolean full = t.hasFullSourceMapping();
		return new RandomAccessible< T >()
		{
			@Override
			public int numDimensions()
			{
				return t.numSourceDimensions();
			}

			@Override
			public RandomAccess< T > randomAccess()
			{
				if ( full )
					return new FullSourceMapMixedRandomAccess< T >( s.randomAccess(), t );
				return new MixedRandomAccess< T >( s.randomAccess(), t );
			}

			@Override
			public RandomAccess< T > randomAccess( final Interval interval )
			{
				if ( full )
					return new FullSourceMapMixedRandomAccess< T >( s.randomAccess(), t );
				return new MixedRandomAccess< T >( s.randomAccess(), t );
			}
		};
	}

	protected RandomAccessible< T > wrapTranslationTransform( final RandomAccessible< T > s, final TranslationTransform t )
	{
		return new RandomAccessible< T >()
		{
			@Override
			public int numDimensions()
			{
				return t.numSourceDimensions();
			}

			@Override
			public TranslationRandomAccess< T > randomAccess()
			{
				return new TranslationRandomAccess< T >( s.randomAccess(), t );
			}

			@Override
			public TranslationRandomAccess< T > randomAccess( final Interval interval )
			{
				return new TranslationRandomAccess< T >( s.randomAccess(), t );
			}
		};
	}

	protected RandomAccessible< T > wrapSlicingTransform( final RandomAccessible< T > s, final SlicingTransform t )
	{
		final boolean full = t.hasFullSourceMapping();
		return new RandomAccessible< T >()
		{
			@Override
			public int numDimensions()
			{
				return t.numSourceDimensions();
			}

			@Override
			public RandomAccess< T > randomAccess()
			{
				if ( full )
					return new FullSourceMapSlicingRandomAccess< T >( s.randomAccess(), t );
				return new SlicingRandomAccess< T >( s.randomAccess(), t );
			}

			@Override
			public RandomAccess< T > randomAccess( final Interval interval )
			{
				if ( full )
					return new FullSourceMapSlicingRandomAccess< T >( s.randomAccess(), t );
				return new SlicingRandomAccess< T >( s.randomAccess(), t );
			}
		};
	}
}
