/*******************************************************************************
*   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.io.Serializable;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.dimple.exceptions.DomainException;
import com.analog.lyric.dimple.model.values.Value;
import com.analog.lyric.dimple.model.variables.Complex;
import com.analog.lyric.dimple.model.variables.Discrete;
import com.analog.lyric.dimple.model.variables.RealJoint;
import com.google.common.math.DoubleMath;

/**
 * Base class for variable domains, which specify a set of valid values for
 * a variable type.
 */
public abstract class Domain implements Serializable
{
	/*-------
	 * State
	 */
	
	private static final long serialVersionUID = 1L;
	private final int _hashCode;
	
	/*--------------
	 * Construction
	 */
	
	Domain(int hashCode)
	{
		_hashCode = hashCode;
	}
	
	/*---------------
	 * Serialization
	 */

	/**
	 * If supported by subclass, returns interned version
	 * of domain.
	 */
	protected Domain intern()
	{
		return this;
	}
	
	protected Object readResolve()
	{
		// Replace with interned version of domain, if available.
		return intern();
	}
	
	/*----------------
	 * Object methods
	 */
	
	@Override
	public final int hashCode()
	{
		return _hashCode;
	}
	
	@Override
	public abstract boolean equals(@Nullable Object other);

	/*----------------
	 * Domain methods
	 */
	
	/**
	 * If domain {@link #isDiscrete()} returns object cast to {@link DiscreteDomain},
	 * otherwise returns null.
	 */
	public @Nullable DiscreteDomain asDiscrete() { return null; }
	
	/**
	 * If domain {@link #isReal()} returns object cast to {@link RealDomain},
	 * otherwise returns null.
	 */
	public @Nullable RealDomain asReal() { return null; }
	
	/**
	 * If domain {@link #isRealJoint()} returns object cast to {@link RealJointDomain},
	 * otherwise returns null.
	 */
	public @Nullable RealJointDomain asRealJoint() { return null; }
	
	/**
	 * If domain {@link #isComplex()} returns object cast to {@link ComplexDomain},
	 * otherwise returns null.
	 */
	public @Nullable ComplexDomain asComplex() { return null; }
	
	/**
	 * The number of dimensions for elements of the domain.
	 * <p>
	 * Returns one for {@linkplain #isScalar() scalar} domains. For {@link RealJointDomain}
	 * and {@link JointDiscreteDomain}, returns the size of the array needed to hold the
	 * components of a single element.
	 * <p>
	 * @since 0.07
	 */
	public int getDimensions()
	{
		return 1;
	}
	
	/**
	 * True if domain only contains values that can be represented using an {@code int}.
	 */
	public boolean hasIntCompatibleValues() { return false; }
	
	/**
	 * True if the elements of the domain are bounded to some subset.
	 * <p>
	 * The default implementation returns true if not {@link #isDiscrete()}.
	 * <p>
	 * @since 0.06
	 */
	public boolean isBounded()
	{
		return !isDiscrete();
	}
	
	/**
	 * True if domain is an instance of {@link Discrete}.
	 * @see #asDiscrete()
	 */
	public boolean isDiscrete() { return false; }
	
	/**
	 * True if values of domain are (32-bit) integers.
	 * <p>
	 * @since 0.05
	 */
	public boolean isIntegral() { return false; }
	
	/**
	 * True if domain is an instance of {@link RealDomain}.
	 * @see #asReal()
	 */
	public boolean isReal() { return false; }
	
	/**
	 * True if domain is an instance of {@link RealJoint}
	 * @see #asRealJoint()
	 */
	public boolean isRealJoint() { return false; }
	
	/**
	 * True if domain is an instance of {@link Complex}
	 * @see #asComplex()
	 */
	public boolean isComplex() { return false; }
	
	/**
	 * True if all values of domain are scalar numbers.
	 * @since 0.05
	 */
	public boolean isNumber() { return true; }
	
	/**
	 * True if all values of domain are (not necessarily scalar) numbers.
	 * @since 0.05
	 */
	public boolean isNumeric() { return true; }
	
	/**
	 * True if all elements of domain are scalar values.
	 * <p>
	 * This is false for {@link RealJointDomain} and {@link JointDiscreteDomain}.
	 * @since 0.05
	 */
	public boolean isScalar() { return true; }
	
	/**
	 * @return true if {@code value} is a valid member of the domain. Implementors
	 * should not throw a cast exception.
	 * @see #valueInDomain(Value)
	 */
	public abstract boolean inDomain(@Nullable Object value);

	/**
	 * @return true if {@code representation} corresponds to a valid member of the domain for
	 * domains that can represent values using an alternate representation, such as the index
	 * of a {@link Discrete} domain with enumerated elements.
	 * <p>
	 * The default implementation simply invokes {@link #inDomain(Object)}.
	 */
	public boolean containsValueWithRepresentation(Object representation)
	{
		return inDomain(representation);
	}

	/**
	 * @return an exception stating that {@code value} is not a member of this domain.
	 */
	public DomainException domainError(@Nullable Object value)
	{
		return new DomainException("'%s' is not a member of domain '%s'", value, this);
	}
	
	/**
	 * Indicate whether {@code value} is a valid member of the domain.
	 * <p>
	 * Semantically equivalent to
	 * <blockquote><tt>
	 * {@link #inDomain(Object) inDomain}(value.{@link Value#getObject getObject}());
	 * </tt></blockquote>
	 * @param value holds the value to be checked.
	 * @since 0.08
	 * @see #inDomain(Object)
	 */
	public abstract boolean valueInDomain(Value value);
	
	/*----------------
	 * Static methods
	 */
	
	/**
	 * True if {@code type} is one of: {@link Integer}, {@link Short}, {@link Byte}.
	 */
	public static boolean isIntCompatibleClass(Class<?> type)
	{
		return Number.class.isAssignableFrom(type) &&
			(type == Integer.class ||
			type == Short.class ||
			type == Byte.class);
	}
	
	public static boolean isIntCompatibleValue(Object value)
	{
		return value instanceof Number && isIntCompatibleValue((Number)value);
	}
	
	public static boolean isIntCompatibleValue(Number value)
	{
		if (isIntCompatibleClass(value.getClass()))
		{
			return true;
		}
		
		if (value instanceof Long)
		{
			return isIntCompatibleValue(((Long)value).longValue());
		}
		
		return isIntCompatibleValue(value.doubleValue());
	}
	
	public static boolean isIntCompatibleValue(double value)
	{
		return DoubleMath.isMathematicalInteger(value) && isIntCompatibleValue((long)value);
	}
	
	public static boolean isIntCompatibleValue(long value)
	{
		return Math.abs(value) <= Integer.MAX_VALUE;
	}
}
