/*******************************************************************************
*   Copyright 2012-2013 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.List;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.collect.ArrayUtil;
import com.analog.lyric.collect.WeakInterner;
import com.analog.lyric.dimple.exceptions.DomainException;
import com.analog.lyric.dimple.factorfunctions.core.FactorFunctionUtilities;
import com.analog.lyric.dimple.model.values.Value;
import com.analog.lyric.dimple.model.variables.Bit;
import com.analog.lyric.util.misc.Internal;
import com.google.common.collect.Interner;
import com.google.common.math.DoubleMath;

import net.jcip.annotations.Immutable;

/**
 * Domain for discrete variables that can take on a values from a predefined finite set
 * in a specified order.
 * <p>
 * While from a set-theoretic perspective two domains with the same elements in different order
 * are equivalent, these objects are only considered equal if the elements occur in the same
 * order because the order is intrinsic in the representation of variable values using the domain.
 */
@Immutable
public abstract class DiscreteDomain extends Domain
{
	private static final long serialVersionUID = 1L;
	
	private static enum InternedDomains
	{
		INSTANCE;
		
		private final Interner<DiscreteDomain> interner = WeakInterner.create();
	}

	@Override
	protected DiscreteDomain intern()
	{
		return InternedDomains.INSTANCE.interner.intern(this);
	}
	
	@SuppressWarnings("unchecked")
	static <D extends DiscreteDomain> D intern(D domain)
	{
		return (D)domain.intern();
	}
	
	/*--------------
	 * Construction
	 */
	
	DiscreteDomain(int hashCode)
	{
		super(hashCode);
	}
	
	/**
	 * Domain consisting of the integers 0 and 1, which is the domain of {@link Bit} variables.
	 */
	public static IntRangeDomain bit()
	{
		return range(0,1);
	}
	
	/**
	 * Domain consisting of the values false and true.
	 */
	public static TypedDiscreteDomain<Boolean> bool()
	{
		return create(false, true);
	}
	
	/**
	 * Returns a discrete domain consisting of the specified elements in the specified order.
	 * <p>
	 * @param elements must either be immutable or must implement {@link Object#equals(Object)} and
	 * {@link Object#hashCode()} methods that do not depend on any mutable state.
	 * @since 0.08
	 */
	public static <T> TypedDiscreteDomain<T> create(List<T> elements)
	{
		// TODO - instead implement in terms of List<T> and implement array versions using Arrays.asList().
		return create(elements.get(0), 1, elements.toArray());
	}
	
	/**
	 * Returns a discrete domain consisting of the specified elements in the specified order.
	 * <p>
	 * @param elements must either be immutable or must implement {@link Object#equals(Object)} and
	 * {@link Object#hashCode()} methods that do not depend on any mutable state.
	 */
	public static <T> TypedDiscreteDomain<T> create(T[] elements)
	{
		return create(elements[0], 1, elements);
	}
	
	public static <T> TypedDiscreteDomain<T> create(T firstElement, Object ... moreElements)
	{
		return create(firstElement, 0, moreElements);
	}
	
	private static <T> TypedDiscreteDomain<T> create(T firstElt, int offset, Object[] moreElements)
	{
		int size = moreElements.length;
		Class<?> eltClass = firstElt.getClass();
		
		if (eltClass.isEnum() && ((Enum<?>)firstElt).ordinal() == 0)
		{
			boolean useEnumDomain = true;
			for (int i = offset; i < size; ++i)
			{
				Object elt = moreElements[i];
				if (elt.getClass() != eltClass || ((Enum<?>)elt).ordinal() != (i + 1))
				{
					useEnumDomain = false;
					break;
				}
			}
			
			if (useEnumDomain)
			{
				@SuppressWarnings({ "unchecked", "rawtypes" })
				TypedDiscreteDomain<T> domain = intern(new EnumDomain(eltClass));
				return domain;
			}
		}
		else if (Integer.class.isAssignableFrom(eltClass))
		{
			// We could eliminate the instanceof checks if the component type of the 'elements'
			// array is Integer...
			if (size > offset && moreElements[offset] instanceof Integer)
			{
				int first = ((Integer)firstElt).intValue();
				int prev = ((Integer)moreElements[offset]).intValue();
				int interval = prev - first;
				
				if (interval > 0)
				{
					boolean useIntRangeDomain = true;

					for (int i = offset+1; i < size; ++i)
					{
						Object element = moreElements[i];
						if (element instanceof Integer)
						{
							int next = prev + interval;
							if (next == ((Integer)element).intValue())
							{
								prev = next;
								continue;
							}
						}
						useIntRangeDomain = false;
						break;
					}

					if (useIntRangeDomain)
					{
						@SuppressWarnings("unchecked")
						TypedDiscreteDomain<T> domain = (TypedDiscreteDomain<T>) range(first, prev, interval);
						return domain;
					}
				}
			}
		}
		else if (Double.class.isAssignableFrom(eltClass))
		{
			if (size > offset && moreElements[offset] instanceof Double)
			{
				double first = ((Double)firstElt).doubleValue();
				double prev = ((Double)moreElements[offset]).doubleValue();
				double interval = prev - first;
				
				if (interval > 0.0)
				{
					double tolerance = DoubleRangeDomain.defaultToleranceForInterval(interval);
					boolean useDoubleRangeDomain = true;

					for (int i = 1+offset; i < size; ++i)
					{
						Object element = moreElements[i];
						if (element instanceof Double)
						{
							double next = prev + interval;
							if (DoubleMath.fuzzyEquals(next, (Double)element, tolerance))
							{
								prev = next;
								continue;
							}
						}
						useDoubleRangeDomain = false;
						break;
					}

					if (useDoubleRangeDomain)
					{
						@SuppressWarnings("unchecked")
						TypedDiscreteDomain<T> domain = (TypedDiscreteDomain<T>) range(first, prev, interval, tolerance);
						return domain;
					}
				}
			}
		}
		
		return intern(new ArrayDiscreteDomain<T>(firstElt, offset, moreElements));
	}
	
	public static <E extends Enum<E>> EnumDomain<E> forEnum(Class<E> enumClass)
	{
		return intern(new EnumDomain<E>(enumClass));
	}
	
	public static <T> JointDiscreteDomain<T> joint(JointDomainIndexer domains)
	{
		return intern(new JointDiscreteDomain<T>(domains));
	}
	
	public static <T> JointDiscreteDomain<T> joint(TypedDiscreteDomain<?>... domains)
	{
		return joint(JointDomainIndexer.create(domains));
	}
	
	public static JointDiscreteDomain<?> joint(DiscreteDomain ... domains)
	{
		return joint(JointDomainIndexer.create(domains));
	}

	/**
	 * Returns discrete domain containing all integers from {@code low} to {@code high}.
	 * @param low is the first element of the domain
	 * @param high is the last element of the domain, which must be greater than or equal to {@code low}
	 * @see #range(int, int, int)
	 */
	public static IntRangeDomain range(int low, int high)
	{
		return intern(new IntRangeDomain(low, high, 1));
	}
	
	/**
	 * Returns discrete domain containing all integers from {@code low} to {@code high} in increments of
	 * specified interval.
	 * <p>
	 * @param low is the first element of the domain
	 * @param high is the upper bound of the domain, which must be greater than {@code low}. Will only
	 * be included in the domain itself if {@code (high - low)} is a multiple of {@code interval}.
	 * @param interval is a positive integer specifying the increment between adjacent elements in the domain.
	 * @see #range(int, int)
	 */
	public static IntRangeDomain range(int low, int high, int interval)
	{
		return intern(new IntRangeDomain(low, high, interval));
	}

	public static DoubleRangeDomain range(double low, double high)
	{
		return DoubleRangeDomain.create(low, high, 1.0, Double.NaN);
	}
	
	public static DoubleRangeDomain range(double low, double high, double interval)
	{
		return DoubleRangeDomain.create(low, high, interval, Double.NaN);
	}

	public static DoubleRangeDomain range(double low, double high, double interval, double tolerance)
	{
		return DoubleRangeDomain.create(low, high, interval, tolerance);
	}
	
	public static FiniteFieldDomain finiteField(int primitivePolynomial)
	{
		return intern(new FiniteFieldDomain(primitivePolynomial));
	}

	/*----------------
	 * Object methods
	 */
	
	/**
	 * True if elements of domain are the same and in the same order.
	 * <p>
	 * The default implementation simply does an elementwise comparison.
	 */
	@Override
	public boolean equals(@Nullable Object thatObj)
	{
		if (this == thatObj)
		{
			return true;
		}
		
		if (!(thatObj instanceof DiscreteDomain))
		{
			return false;
		}
		
		DiscreteDomain that = (DiscreteDomain)thatObj;
		if (size() != that.size() || hashCode() != that.hashCode())
		{
			return false;
		}
		
		for (int i = 0, end = size(); i < end; ++i)
		{
			if (!this.getElement(i).equals(that.getElement(i)))
			{
				return false;
			}
		}
		
		return true;
	}
	
	/*----------------
	 * Domain methods
	 */
	
	@Override
	public final DiscreteDomain asDiscrete()
	{
		return this;
	}
	
	@Override
	public boolean hasIntCompatibleValues()
	{
		return isIntCompatibleClass(getElementClass());
	}
	
	@Override
	public boolean inDomain(@Nullable Object value)
	{
		return (getIndex(value) >= 0);
	}
	
	@Override
	public boolean isIntegral()
	{
		return isIntCompatibleClass(getElementClass());
	}
	
	/**
	 * @return true if {@code value} is a {@link Number} representing a valid
	 * integer index into the array of elements returned by {@link #getElements()}.
	 */
	@Override
	public boolean containsValueWithRepresentation(Object value)
	{
		if (value instanceof Number)
		{
			Number number = (Number)value;
			int index = number.intValue();
			return index >=0 && index < size();
		}
		
		return false;
	}
	
	@Override
	public final boolean isDiscrete()
	{
		return true;
	}
	
	@Override
	public boolean valueInDomain(Value value)
	{
		if (this.equals(value.getDomain()))
		{
			final int index = value.getIndex();
			return index >= 0 && index < size();
		}
		else
		{
			return inDomain(value.getObject());
		}
	}
	
	/*------------------------
	 * DiscreteDomain methods
	 */
	
	/**
	 * Returns a view of the domain as an immutable list.
	 * @since 0.08
	 */
	public abstract List<? extends Object> asList();
	
	/**
	 * If this is a {@link TypedDiscreteDomain} with element type that extends {@code elementClass}
	 * returns this object, otherwise null.
	 */
	public abstract @Nullable <T> TypedDiscreteDomain<T> asTypedDomain(Class<T> elementClass);
	
	/**
	 * Returns ith element as a double
	 * @since 0.08
	 */
	public double getDoubleElement(int i)
	{
		return FactorFunctionUtilities.toDouble(getElement(i));
	}
	
	/**
	 * Common superclass of all elements.
	 */
	public abstract Class<?> getElementClass();
	
	/**
	 * Returns the value of the ith element in the domain.
	 * @param i must be in the range [0,{@link #size()}-1].
	 * @throws IndexOutOfBoundsException if {@code i} is not in required range.
	 */
	public abstract Object getElement(int i);
	
	/**
	 * Returns an array containing all the elements of the domain in their canonical order.
	 * <p>
	 * Because this allocates memory and costs linear time to compute, it is usually better to use
	 * {@link #getElement(int)} to access elements one at a time and {@link #size()} to get the
	 * number of elements.
	 * <p>
	 * @see #getElements(Object[])
	 */
	public Object[] getElements()
	{
		return getElements(null);
	}
	
	/**
	 * Returns an array containing all of the elements of the domain in their canonical
	 * order using provided {@code array} if possible.
	 * <p>
	 * If {@code array} has a component type compatible with {@link #getElementClass()} and
	 * length at least as large as {@link #size()} it will be filled in with the domain elements
	 * and returned. If {@code array} has a compatible type but is not long enough, a new
	 * array will be returned with the same component type as {@code array}. Otherwise a new
	 * array will be returned with component type same as {@link #getElementClass()}.
	 */
	public <T> T[] getElements(@Nullable T[] array)
	{
		final int length = size();
		
		array = ArrayUtil.allocateArrayOfType(getElementClass(), array, length);
		
		for (int i = 0; i < length; ++i)
		{
			@SuppressWarnings("unchecked")
			T element = (T) getElement(i);
			array[i] = element;
		}
		
		return array;
	}
	
	/**
	 * Returns ith element as an integer
	 * @since 0.08
	 */
	public int getIntElement(int i)
	{
		return FactorFunctionUtilities.toInteger(getElement(i));
	}

	/**
	 * The number of elements in the domain.
	 */
	public abstract int size();
	
	@Override
	public String toString()
	{
		final int maxToShow = 10;
		final int size = size();
		final int n = Math.min(size, maxToShow) - 1;
		StringBuilder sb = new StringBuilder("{");
		for (int i = 0; i < n; ++i)
		{
			sb.append(getElement(i));
			sb.append(",");
		}
		if (n < size - 1)
		{
			sb.append("...,");
		}
		sb.append(getElement(size-1));
		sb.append("}");
		return sb.toString();
	}
	
	// Find the list of elements corresponding to the value; return -1 if not a valid value
	/**
	 * Returns the index of {@code value} in this domains elements or else -1 if
	 * {@code value} is not an element of this domain.
	 * @see #getIndexOrThrow(Object)
	 */
	public abstract int getIndex(@Nullable Object value);
	
	/**
	 * Like {@link #getIndex(Object)} but throws a {@link DomainException} instead of returning
	 * -1 on failure.
	 */
	public int getIndexOrThrow(@Nullable Object value)
	{
		int index = getIndex(value);
		if (index < 0)
		{
			throw domainError(value);
		}
		return index;
	}

	/**
	 * @deprecated Use {@link #inDomain} instead.
	 */
	@Deprecated
	public boolean isElementOf(Object value)
	{
		return (getIndex(value) >= 0);
	}

	/*------------------
	 * Internal methods
	 */
	
	/**
	 * Verifies that {@code index} is in the range [0,{@link #size}-1].
	 * @throws IndexOutOfBoundsException if index is not in range.
	 * @category internal
	 */
	@Internal
	public void assertIndexInBounds(int index)
	{
		assertIndexInBounds(index, size());
	}
	
	/*-------------------
	 * Protected methods
	 */
	
	/**
	 * Verifies that {@code index} is in the range [0,size-1].
	 * @throws IndexOutOfBoundsException if index is not in range.
	 */
	protected static void assertIndexInBounds(int index, int size)
	{
		// We want this check to be fast. This converts index to a long and masks the low-32 bits.
		// As a result, all negative index values are going to end up as large positive numbers
		// allowing us to do this in a single compare.
		if ((index & 0xFFFFFFFFL) >= size)
		{
			throw new IndexOutOfBoundsException(String.format("Index '%d' is not in range [%d,%d]", index, 0, size -1));
		}
	}
}
