/*******************************************************************************
 *   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.benchmarking.utils.doublespace;

import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.jdt.annotation.Nullable;
import com.google.common.collect.Iterables;

/**
 * Combines indexers to produce multidimensional indexes.
 */
public class JointIndexer
{
	private static int computeCardinality(List<Indexer> indexers)
	{
		int result = 1;
		for (final Indexer indexer : indexers)
		{
			result *= indexer.getCardinality();
		}
		return result;
	}

	private final int _cardinality;

	private final Set<int[]> _coordinatesSet = Collections.unmodifiableSet(new AbstractSet<int[]>()
	{

		@Override
		public Iterator<int[]> iterator()
		{
			return new CoordinatesIterator();
		}

		@Override
		public int size()
		{
			return getCardinality();
		}

	});

	private final int[] _dimensions;

	private final List<Indexer> _indexers;

	/**
	 * Constructs a joint indexer given an array of indexers.
	 * 
	 * @param indexers
	 *            The individual indexers, one for each dimension to index.
	 */
	public JointIndexer(Indexer... indexers)
	{
		this(Arrays.asList(indexers));
	}

	/**
	 * Constructs a joint indexer given an Iterable of indexers.
	 * 
	 * @param indexers
	 *            The individual indexers, one for each dimension to index.
	 */
	public JointIndexer(Iterable<Indexer> indexers)
	{
		_indexers = new ArrayList<Indexer>();
		Iterables.addAll(_indexers, indexers);
		_cardinality = computeCardinality(_indexers);
		_dimensions = new int[_indexers.size()];
		for (int i = 0; i < _dimensions.length; i++)
		{
			_dimensions[i] = _indexers.get(i).getCardinality();
		}
	}

	/**
	 * Gets a set of all joint indexes, representing the cross product of the
	 * individual indexers.
	 */
	public Set<int[]> coordinatesSet()
	{
		return _coordinatesSet;
	}

	/**
	 * Gets the quantity of joint indexes.
	 */
	public int getCardinality()
	{
		return _cardinality;
	}

	/**
	 * Gets an array containing the cardinality of each of the dimensions.
	 */
	public int[] getDimensions()
	{
		return _dimensions.clone();
	}

	/**
	 * Gets the quantity of dimensions.
	 */
	public int getDimensionsCount()
	{
		return _dimensions.length;
	}

	/**
	 * Gets the nth represented index.
	 * 
	 * @param n
	 *            The zero-based position of the desired index.
	 */
	public Indexer getIndexer(int n)
	{
		return _indexers.get(n);
	}

	private class CoordinatesIterator implements Iterator<int[]>
	{
		private @Nullable int[] _current;

		private final List<Iterator<Integer>> _iterators;

		public CoordinatesIterator()
		{
			final int dimensions = getDimensionsCount();
			_iterators = new ArrayList<Iterator<Integer>>(dimensions);
			for (int i = 0; i < dimensions; i++)
			{
				_iterators.add(_indexers.get(i).iterator());
			}
		}

		@Override
		public boolean hasNext()
		{
			final int dimensions = getDimensionsCount();
			for (int i = dimensions - 1; i >= 0; i--)
			{
				if (_iterators.get(i).hasNext())
				{
					return true;
				}
			}
			return false;
		}

		@Override
		public int[] next()
		{
			final int dimensions = getDimensionsCount();
			int[] current = _current;
			if (current == null)
			{
				current = _current = new int[dimensions];
				for (int i = dimensions - 1; i >= 0; i--)
				{
					final Iterator<Integer> it = _iterators.get(i);
					if (it.hasNext())
					{
						current[i] = it.next();
					}
				}
			}
			else
			{
				for (int i = dimensions - 1; i >= 0; i--)
				{
					final Iterator<Integer> it = _iterators.get(i);
					if (it.hasNext())
					{
						current[i] = it.next();
						for (int j = i + 1; j < dimensions; j++)
						{
							final Iterator<Integer> it_j = _indexers.get(j).iterator();
							_iterators.set(j, it_j);
							if (it_j.hasNext())
							{
								current[j] = it_j.next();
							}
							else
							{
								throw new InternalError();
							}
						}
						break;
					}
				}

			}
			return current.clone();
		}

		@Override
		public void remove()
		{
			throw new UnsupportedOperationException();
		}

	}

}
