/*******************************************************************************
*   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.lang.reflect.Array;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.collect.ArrayUtil;
import com.analog.lyric.dimple.exceptions.DomainException;

import net.jcip.annotations.Immutable;

/**
 * A discrete domain representing the Cartesian product of other discrete domains.
 * Its elements are Java Object[] arrays with the nth element holding an element
 * of the nth component domain.
 * <p>
 * It is represented efficiently using a {@link JointDomainIndexer}, so the full
 * combinatoric list of elements does not need to be produced unless someone
 * calls {@link #getElements()}.
 * <p>
 * Construct instances using {@link DiscreteDomain#joint(DiscreteDomain...)} or
 * {@link DiscreteDomain#joint(JointDomainIndexer)}.
 */
@Immutable
public class JointDiscreteDomain<Element> extends TypedDiscreteDomain<Element[]>
{
	/*-------
	 * State
	 */
	
	private static final long serialVersionUID = 1L;
	
	private final JointDomainIndexer _domains;
	private final Class<Element[]> _elementClass;
	
	/*--------------
	 * Construction
	 */
	
	JointDiscreteDomain(JointDomainIndexer domains)
	{
		super(domains.hashCode());
		_domains = domains;
		@SuppressWarnings("unchecked")
		Class<Element[]> elementClass = (Class<Element[]>) Array.newInstance(domains.getElementClass(), 0).getClass();
		_elementClass = elementClass;
	}
	
	/*----------------
	 * Object methods
	 */
	
	@Override
	public boolean equals(@Nullable Object that)
	{
		if (this == that)
		{
			return true;
		}
	
		if (that instanceof JointDiscreteDomain)
		{
			return ((JointDiscreteDomain<?>)that)._domains.equals(_domains);
		}
		
		return false;
	}
	
	@Override
	public String toString()
	{
		StringBuilder sb = new StringBuilder();
		boolean comma = false;
		for (DiscreteDomain domain : _domains)
		{
			if (comma)
			{
				sb.append('x');
			}
			else
			{
				comma = true;
			}
			sb.append(domain.toString());
		}
		return sb.toString();
	}

	/*------------------------
	 * DiscreteDomain methods
	 */
	
	/**
	 * True if {@code value} is an array of length {@link #getDimensions()} for which
	 * 
	 */
	@Override
	public boolean inDomain(@Nullable Object value)
	{
		Object[] values = ArrayUtil.toArray(value);
		if (values != null)
		{
			int dimensions = getDimensions();
			if (values.length == dimensions)
			{
				for (int i = 0; i < dimensions; ++i)
				{
					DiscreteDomain discrete = _domains.get(i);
					if (!discrete.inDomain(values[i]))
					{
						return false;
					}
				}
				return true;
			}
		}
		return false;
	}
	
	@Override
	public boolean containsValueWithRepresentation(Object value)
	{
		if (value instanceof Number)
		{
			return super.containsValueWithRepresentation(value);
		}
		
		int[] indices = ArrayUtil.toIntArray(value);
		if (indices != null)
		{
			final int dimensions = getDimensions();
			if (indices.length == dimensions)
			{
				for (int i = 0; i < dimensions; ++i)
				{
					int index = indices[i];
					if (index < 0 || index >= _domains.getDomainSize(i))
					{
						return false;
					}
				}
				return true;
			}
		}
		return false;
	}

	@Override
	public final Class<Element[]> getElementClass()
	{
		return _elementClass;
	}
	
	/**
	 * Returns ith element in domain.
	 * <p>
	 * This allocates a new array. You can copy into an existing array using {@link #getElement(int, Object[])}.
	 * @param i must be in the range [0, {@link #size()}-1].
	 */
	@Override
	public Element[] getElement(int i)
	{
		return getElement(i, null);
	}

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

	@Override
	public int getIndex(@Nullable Object value)
	{
		try
		{
			return getIndexOrThrow(value);
		}
		catch (DomainException ex)
		{
			return -1;
		}
	}
	
	@Override
	public int getIndexOrThrow(@Nullable Object value)
	{
		if (value instanceof Object[])
		{
			return getIndexFromSubelements((Object[])value);
		}
		else if (value != null && value.getClass().isArray())
		{
			int size = getDimensions();
			Object[] values = new Object[size];
			for (int i = 0; i < size; ++i)
			{
				values[i] = Array.get(value, i);
			}
			return getIndexFromSubelements(values);
		}
		
		throw domainError(value);
	}
	
	@Override
	public final boolean isNumber()
	{
		return false;
	}
	
	@Override
	public final boolean isNumeric()
	{
		return Number.class.isAssignableFrom(_elementClass);
	}
	
	@Override
	public final boolean isScalar()
	{
		return false;
	}
	
	/*-----------------------------
	 * JointDiscreteDomain methods
	 */

	/**
	 * Returns the number of dimensions, or subdomains that make up this joint domain.
	 * Same as size of {@link #getDomainIndexer()}.
	 */
	@Override
	public final int getDimensions()
	{
		return _domains.size();
	}
	
	/**
	 * Returns the underlying {@link JointDomainIndexer} that represents this domain.
	 */
	public final JointDomainIndexer getDomainIndexer()
	{
		return _domains;
	}
	
	/**
	 * Writes the subelements of the ith element in the domain into {@code array} and returns it.
	 * <p>
	 * @param i must be in the range [0, {@link #size()}-1].
	 * @param array will only be used if non-null and no shorter than {@link #size()}, otherwise
	 * a new array will be allocated.
	 * @return array containing ith element, which will be the same as {@code array} provided it was big enough.
	 * @see #getElement(int)
	 * @see #getElementIndices(int, int[])
	 */
	public <T> T[] getElement(int i, @Nullable T[] array)
	{
		return _domains.jointIndexToElements(i, array);
	}
	
	/**
	 * Returns any array containing the individual subdomain indexes for the
	 * ith element in this domain.
	 * <p>
	 * This is equivalent to calling {@link #getElementIndices(int, int[])} with a null second argument.
	 * <p>
	 * @param i must be in the range [0, {@link #size()}-1].
	 * @return newly allocated array of indices
	 */
	public int[] getElementIndices(int i)
	{
		return _domains.jointIndexToIndices(i, null);
	}

	/**
	 * Writes the individual subdomain indexes for the ith element in this domain into
	 * provided {@code array} and returns it.
	 * <p>
	 * This is equivalent to calling {@link #getDomainIndexer()}.jointIndexToIndices(i, array)
	 * <p>
	 * @param i must be in the range [0, {@link #size()}-1].
	 * @param array must be at least {@link #size()} long or else a new array will be allocated.
	 * @return array containing indices, which will be same as provided {@code array} if it is large enough
	 * @see #getElementIndices(int)
	 * @see #getElement(int, Object[])
	 */
	public int[] getElementIndices(int i, int[] array)
	{
		return _domains.jointIndexToIndices(i, array);
	}

	public final int getIndexFromSubelements(Object ... values)
	{
		return _domains.jointIndexFromElements(values);
	}
	
	public final int getIndexFromIndices(int ... indices)
	{
		return _domains.jointIndexFromIndices(indices);
	}

}
