/*******************************************************************************
*   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 net.jcip.annotations.Immutable;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.dimple.model.values.Value;

@Immutable
public class StandardJointDomainIndexer extends JointDomainIndexer
{
	/*-------
	 * State
	 */
	
	private static final long serialVersionUID = 1L;
	
	/**
	 * Contains cumulative products of domain sizes, such that _products[0] == 1
	 * and otherwise _products[i] == getDomainSize(i-1) * _products[i-1].
	 */
	private final int[] _products;
	
	/**
	 * The joint cardinality of all of the domains: the product of all of the domain
	 * sizes.
	 */
	private final int _cardinality;
	
	private final int _sumOfSizes;
	
	/*--------------
	 * Construction
	 */
	
	StandardJointDomainIndexer(int hashCode, DiscreteDomain[] domains)
	{
		super(hashCode, domains);
		
		final int nDomains = domains.length;
		
		_products = new int[nDomains];
		int product = 1;
		int sum = 0;
		for (int i = 0; i < nDomains; ++i)
		{
			final int size = domains[i].size();
			_products[i] = product;
			product *= size;
			sum += size;
		}
		_cardinality = product;
		_sumOfSizes = sum;
	}
	
	StandardJointDomainIndexer(DiscreteDomain[] domains)
	{
		this(computeHashCode(domains), domains);
	}
	
	/*----------------------------
	 * JointDomainIndexer methods
	 */
	
	/**
	 * The number of possible combinations of all domain elements. Equal to the product of
	 * all of the domain sizes.
	 * <p>
	 * @see #getInputCardinality()
	 * @see #getOutputCardinality()
	 */
	@Override
	public final int getCardinality()
	{
		return _cardinality;
	}
	
	/**
	 * Returns the index of the ith domain designated as an input domain.
	 * <p>
	 * This is equivalent to returning the ith element of {@link #getInputDomainIndices()} but
	 * without having to allocate and copy an array.
	 * <p>
	 * @throws ArrayIndexOutOfBoundsException if i is not in range [0, {@link #getInputSize()}-1]
	 * (will always throw if not {@link #isDirected()}.
	 */
	@Override
	public int getInputDomainIndex(int i)
	{
		throw new ArrayIndexOutOfBoundsException();
	}
	
	/**
	 * The number of possible combinations of output domain elements. Equal to the product of
	 * all of the output domain sizes. Will be the same as {@link #getCardinality()} if not {@link #isDirected()}.
	 * @see #getInputCardinality()
	 */
	@Override
	public int getOutputCardinality()
	{
		return _cardinality;
	}
	
	/**
	 * Returns amount by which joint index returned by {@link #jointIndexFromIndices(int...)} changes
	 * when ith element index changes by 1.
	 * <p>
	 * This can be used to iterate over the joint indexes for one dimension for fixed values of all of
	 * the other dimensions.
	 * <p>
	 * @see #getUndirectedStride(int)
	 */
	@Override
	public int getStride(int i)
	{
		return _products[i];
	}
	
	@Override
	public int getSumOfDomainSizes()
	{
		return _sumOfSizes;
	}
	
	/**
	 * Returns amount by which joint index returned by {@link #undirectedJointIndexFromIndices(int...)} changes
	 * when ith element index changes by 1.
	 * <p>
	 * This can be used to iterate over the joint indexes for one dimension for fixed values of all of
	 * the other dimensions.
	 * <p>
	 * @see #getStride(int)
	 */
	@Override
	public final int getUndirectedStride(int i)
	{
		return _products[i];
	}
	
	@Override
	public final int undirectedJointIndexFromElements(Object ... elements)
	{
		final DiscreteDomain[] domains = _domains;
		final int[] products = _products;
		int joint = domains[0].getIndexOrThrow(elements[0]);
		for (int i = 1, end = products.length; i < end; ++i)
		{
			joint += products[i] * domains[i].getIndexOrThrow(elements[i]);
		}
		return joint;
	}

	@Override
	public final int undirectedJointIndexFromIndices(int ... indices)
	{
		final int length = size();
		int joint = indices[0]; // _products[0] is 1, so we can skip the multiply
		for (int i = 1, end = length; i != end; ++i) // != is slightly faster than < comparison
		{
			joint += indices[i] * _products[i];
		}
		return joint;
	}
	
	@Override
	public final int undirectedJointIndexFromValues(Value ... values)
	{
		final int length = size();
		int joint = values[0].getIndex(); // _products[0] is 1, so we can skip the multiply
		for (int i = 1, end = length; i != end; ++i) // != is slightly faster than < comparison
		{
			joint += values[i].getIndex() * _products[i];
		}
		return joint;
	}

	@Override
	public final <T> T[] undirectedJointIndexToElements(int jointIndex, @Nullable T[] elements)
	{
		final DiscreteDomain[] domains = _domains;
		final int[] products = _products;

		elements = allocateElements(elements);
		
		int product;
		for (int i = products.length; --i >= 0;)
		{
			final int index = jointIndex / (product = products[i]);
			@SuppressWarnings("unchecked")
			T element = (T) domains[i].getElement(index);
			elements[i] = element;
			jointIndex -= index * product;
		}
		return elements;
	}
	
	@Override
	public final Value[] undirectedJointIndexToValues(int jointIndex, Value[] values)
	{
		final DiscreteDomain[] domains = _domains;
		final int[] products = _products;
		
		int product;
		for (int i = products.length; --i >= 0;)
		{
			final Value value = values[i];
			final DiscreteDomain domain = domains[i];
			final int index = jointIndex / (product = products[i]);
			if (value.getDomain() == domain)
			{
				// If domain matches, then use the faster setIndex method.
				//
				// Because domains are interned, the == check should be sufficient the vast
				// majority of the time, and in the unlikely event it is not, setObject will
				// still do the right thing.
				value.setIndex(index);
			}
			else
			{
				value.setObject(domain.getElement(index));
			}
			jointIndex -= index * product;
		}
		return values;
	}
	

	/**
	 * Computes element index for a single domain from a joint index using undirected ordering
	 * of domains.
	 * <p>
	 * This is like {@link #undirectedJointIndexToIndices} but only computes one element index.
	 * <p>
	 * @param jointIndex must be in range [0, {@link #getCardinality()}-1].
	 * @param domainIndex must be in range [0, {@link #size()}-1].
	 * <p>
	 * @see #jointIndexToElementIndex(int, int)
	 */
	@Override
	public int undirectedJointIndexToElementIndex(int jointIndex, int domainIndex)
	{
		return (jointIndex / _products[domainIndex]) % _domains[domainIndex].size();
	}
	
	@Override
	public final int[] undirectedJointIndexToIndices(int jointIndex, @Nullable int[] indices)
	{
		final int[] products = _products;
		
		indices = allocateIndices(indices);
		
		int product;
		for (int i = products.length; --i >= 0;)
		{
			final int index = jointIndex / (product = products[i]);
			indices[i] = index;
			jointIndex -= index * product;
		}
		return indices;
	}

	@Override
	public boolean supportsJointIndexing()
	{
		return true;
	}
	
	@Override
	public boolean supportsOutputIndexing()
	{
		return true;
	}
}
