/*******************************************************************************
*   Copyright 2014 Analog Devices, Inc.
*
*   Licensed under the Apache License, Version 2.0 (the "License");
*   you may not use this file except in compliance with the License.
*   You may obtain a copy of the License at
*
*       http://www.apache.org/licenses/LICENSE-2.0
*
*   Unless required by applicable law or agreed to in writing, software
*   distributed under the License is distributed on an "AS IS" BASIS,
*   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*   See the License for the specific language governing permissions and
*   limitations under the License.
********************************************************************************/

package com.analog.lyric.dimple.model.domains;

import java.util.Arrays;
import java.util.Iterator;

import org.eclipse.jdt.annotation.Nullable;

import net.jcip.annotations.NotThreadSafe;

/**
 * An iterator over any array of integer indices each with a specified bounds
 * iterating over the index at position n before that at n+1. This is the
 * same as dictionary order if the indices are read from back to front.
 * <p>
 * For example, the following code:
 * <pre>
 * 	   DiscreteIndicesIterator iter =  new DiscreteIndicesIterator(2, 3);
 *     while (iter.hasNext())
 *     {
 *        System.out.format("%s\n", Arrays.toString(iter.next()));
 *     }
 * </pre>
 * will produce the output:
 * <pre>
 *     [0,0]
 *     [1,0]
 *     [0,1]
 *     [1,1]
 *     [0,2]
 *     [1,2]
 * </pre>
 * Unlike most iterators, this always returns the same int[] instance, so if you want
 * a fresh copy, you must make sure to clone it.
 * <p>
 * As a special case, when passed an empty domain list, the iterator will still iterate once!
 * 
 * @since 0.05
 */
@NotThreadSafe
public final class DiscreteIndicesIterator implements Iterator<int[]>
{
	/**
	 * Maximum value for each index.
	 */
	private final int[] _limits;
	/**
	 * Index values
	 */
	private final int[] _indices;
	/**
	 * The highest numbered index of {@code _indices} that has not already reached its maximum value.
	 */
	private int _lastNotAtLimit;
	/**
	 * The highest numbered index of {@code _limits} that is non-zero.
	 */
	private final int _lastNonZeroLimit;
	private boolean _doOnce;

	/*--------------
	 * Construction
	 */

	/**
	 * Constructs an iterator over the indices over the specified {@code domains}.
	 * If {@code domains} isa {@link JointDomainIndexer}, then this will produce
	 * the same order as the undirected joint index order.
	 * <p>
	 * @see #DiscreteIndicesIterator(DomainList, int[])
	 */
	public DiscreteIndicesIterator(DomainList<DiscreteDomain> domains)
	{
		this(domains, null);
	}
	
	/**
	 * Constructs an iterator over the indices over the specified {@code domains}
	 * using the specified {@code indices} array.

	 * @param indices must be null or have length equal to {@code domains} size.
	 * 
	 * @see #DiscreteIndicesIterator(DomainList)
	 */
	public DiscreteIndicesIterator(DomainList<DiscreteDomain> domains, @Nullable int[] indices)
	{
		this(limitsFromDomains(domains), indices, false);
	}
	
	/**
	 * Constructs an iterator over the indices each in the range [0, sizes[i] - 1].

	 * @see #DiscreteIndicesIterator(int[], int[])
	 */
	public DiscreteIndicesIterator(int ... sizes)
	{
		this(sizes, null);
	}
	
	/**
	 * Constructs an iterator over the indices each in the range [0, sizes[i] - 1].
	 * 
	 * @param indices must be null or have length equal to {@code domains} size.
	 * 
	 * @see #DiscreteIndicesIterator(int[], int[])
	 */
	public DiscreteIndicesIterator(int[] sizes, @Nullable int[] indices)
	{
		this(sizes, indices, true);
	}
	
	private DiscreteIndicesIterator(int[] sizesOrLimits, @Nullable int[] indices, boolean sizes)
	{
		final int dimensions = sizesOrLimits.length;
		
		if (sizes)
		{
			_limits = new int[dimensions];
			for (int i = 0; i < dimensions; ++i)
			{
				_limits[i] = sizesOrLimits[i] - 1;
			}
		}
		else
		{
			_limits = sizesOrLimits;
		}
		_indices = indices != null ? indices : new int[dimensions];
		
		int lastNonZeroLimit = -1;
		for (int i = dimensions; --i>=0;)
		{
			if (_limits[i] > 0)
			{
				lastNonZeroLimit = i;
				break;
			}
		}
		_lastNonZeroLimit = lastNonZeroLimit;
		
		reset();
	}
	
	private static int[] limitsFromDomains(DomainList<DiscreteDomain> domains)
	{
		final int dimensions = domains.size();
		final int[] limits = new int[dimensions];
		
		for (int i = 0; i < dimensions; ++i)
		{
			limits[i] = domains.get(i).size() - 1;
		}
		
		return limits;
	}
	
	/*------------------
	 * Iterator methods
	 */
	
	@Override
	public boolean hasNext()
	{
		return _lastNotAtLimit >= 0 || _doOnce;
	}

	@Override
	public int[] next()
	{
		_doOnce = false;
		
		for (int i = 0; i <= _lastNotAtLimit; ++i)
		{
			final int limit = _limits[i];
			int val = _indices[i] + 1;
			
			if (val <= limit)
			{
				_indices[i] = val;
				if (val == limit && i == _lastNotAtLimit)
				{
					while (--i>0 && _limits[i] == 0) {}
					_lastNotAtLimit = i;
				}
				break;
			}
			else // val > limit
			{
				_indices[i] = 0;
			}
		}
			
		return _indices;
	}

	@Override
	public void remove()
	{
		throw new UnsupportedOperationException(getClass().getSimpleName() + ".remove");
	}

	/*---------------------------------
	 * DiscreteIndicesIterator methods
	 */
	
	/**
	 * Reset position back to head of iterator.
	 */
	public void reset()
	{
		Arrays.fill(_indices, 0);
		if (_indices.length > 0)
		{
			_indices[0] = -1;
		}
		else
		{
			_doOnce = true;
		}
		_lastNotAtLimit = _lastNonZeroLimit;
	}
}
