/*******************************************************************************
*   Copyright 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.values;

import static java.util.Objects.*;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.Objects;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.dimple.data.DataRepresentationType;
import com.analog.lyric.dimple.data.IDatum;
import com.analog.lyric.dimple.factorfunctions.core.FactorFunctionUtilities;
import com.analog.lyric.dimple.model.domains.DiscreteDomain;
import com.analog.lyric.dimple.model.domains.Domain;
import com.analog.lyric.dimple.model.domains.DomainList;
import com.analog.lyric.dimple.model.domains.DoubleRangeDomain;
import com.analog.lyric.dimple.model.domains.FiniteFieldDomain;
import com.analog.lyric.dimple.model.domains.FiniteFieldNumber;
import com.analog.lyric.dimple.model.domains.IntDomain;
import com.analog.lyric.dimple.model.domains.IntRangeDomain;
import com.analog.lyric.dimple.model.domains.ObjectDomain;
import com.analog.lyric.dimple.model.domains.RealDomain;
import com.analog.lyric.dimple.model.domains.RealJointDomain;
import com.analog.lyric.dimple.model.domains.TypedDiscreteDomain;

/**
 * Holder for a values for a given {@link Domain}.
 * <p>
 * Subclasses are implemented to take advantage of knowledge of domain values.
 * 
 * @since 0.05
 */
public abstract class Value implements IDatum, Cloneable, Serializable
{
	private static final long serialVersionUID = 1L;

	/*--------------
	 * Construction
	 */
	
	/**
	 * Creates an immutable {@link Value} instance appropriate for given domain and value.
	 * @since 0.08
	 * @see #create(Domain)
	 */
	public static Value constant(Domain domain, @Nullable Object value)
	{
		if (value instanceof Value)
		{
			Value v = (Value)value;
			if (domain.equals(v.getDomain()))
			{
				return v.immutableClone();
			}
			value = v.getObject();
		}
		
		if (domain instanceof DiscreteDomain)
		{
			return constantDiscrete((DiscreteDomain)domain, requireNonNull(value));
		}

		if (domain.isReal())
		{
			return constantReal(FactorFunctionUtilities.toDouble(value));
		}
		
		if (domain.isRealJoint())
		{
			return constantRealJoint((double[])requireNonNull(value));
		}
		
		if (domain instanceof IntDomain)
		{
			return new ConstantIntValue(FactorFunctionUtilities.toInteger(value));
		}
		
		return new ConstantObjectValue(value);
	}
	
	/**
	 * Creates an immutable {@link Value} instance appropriate for given value.
	 * @since 0.08
	 * @see #create(Object)
	 */
	public static Value constant(@Nullable Object value)
	{
		if (value instanceof Number)
		{
			Number number = (Number)value;
			
			if (number instanceof Integer || number instanceof Short || number instanceof Byte)
			{
				return new ConstantIntValue(number.intValue());
			}
			else if (number instanceof FiniteFieldNumber)
			{
				return new ConstantFiniteFieldValue((FiniteFieldNumber)number);
			}
			else
			{
				// TODO: There is no LongValue so long gets returned as a Real
				return constantReal(number.doubleValue());
			}
		}
		else if (value instanceof Boolean)
		{
			return new ConstantDiscreteValue(DiscreteDomain.bool(), (Boolean)value ? 1 : 0);
		}
		else if (value instanceof double[])
		{
			return new ConstantRealJointValue((double[])value);
		}
		else if (value instanceof Value)
		{
			return ((Value)value).immutableClone();
		}
		
		return new ConstantObjectValue(value);
	}

	/**
	 * Create an immutable {@link DiscreteValue} with given domain and value.
	 * @since 0.08
	 * @see #constantWithIndex(DiscreteDomain, int)
	 */
	public static DiscreteValue constantDiscrete(DiscreteDomain domain, Object value)
	{
		return constantWithIndex(domain, domain.getIndexOrThrow(value));
	}

	/**
	 * Creates an immutable {@link RealValue} with unbounded domain and given value.
	 * @since 0.08
	 */
	public static RealValue constantReal(double value)
	{
		return new ConstantRealValue(value);
	}
	
	/**
	 * Creates an immutable {@link RealJointValue} with unbounded domain and given value.
	 * @since 0.08
	 */
	public static RealJointValue constantRealJoint(double ... value)
	{
		return new ConstantRealJointValue(value.clone());
	}
	
	/**
	 * Create an immutable {@link DiscreteValue} with given domain and value.
	 * @since 0.08
	 * @see #constantDiscrete(DiscreteDomain, Object)
	 * @see #createWithIndex(DiscreteDomain, int)
	 */
	public static DiscreteValue constantWithIndex(DiscreteDomain domain, int index)
	{
		return new ConstantDiscreteValue(domain, index);
	}
	
	/**
	 * Creates a {@code Value} instance appropriate to the given {@code domain}
	 * with a default value depending on the domain: zero for numeric domains,
	 * the first element for discrete domains, and null for {@link ObjectDomain}.
	 * <p>
	 * If {@code domain} is null, an {@link ObjectValue} will be returned.
	 */
	public static Value create(@Nullable Domain domain)
	{
		if (domain != null)
		{
			DiscreteDomain discrete = domain.asDiscrete();
			if (discrete != null)
			{
				return create(discrete);
			}

			RealDomain real = domain.asReal();
			if (real != null)
			{
				return create(real);
			}


			RealJointDomain realJoint = domain.asRealJoint();
			if (realJoint != null)
			{
				return create(realJoint);
			}

			if (domain.isIntegral())
			{
				return new IntValue();
			}

			assert(domain == ObjectDomain.instance());
		}
		
		return new ObjectValue();
	}
	
	/**
	 * Creates a {@link RealValue} instance for given {@code domain}.
	 */
	public static RealValue create(RealDomain domain)
	{
		return new RealValue(0.0);
	}
	
	/**
	 * Creates a {@link RealJointValue} instance with dimensions matching {@code domain}.
	 * @since 0.08
	 */
	public static RealJointValue create(RealJointDomain domain)
	{
		return new RealJointValue(domain);
	}
	
	/**
	 * Creates a {@link DiscreteValue} instance for given {@code domain}.
	 */
	public static DiscreteValue create(DiscreteDomain domain)
	{
		if (domain.isIntegral())
		{
			if (domain instanceof IntRangeDomain)
			{
				IntRangeDomain rangeDomain = (IntRangeDomain)domain;
				if (rangeDomain.getLowerBound() == 0 && rangeDomain.getInterval() == 1)
				{
					return new SimpleIntRangeValue(rangeDomain);
				}
				else
				{
					return new IntRangeValue(rangeDomain);
				}
			}
			else if (domain instanceof FiniteFieldDomain)
			{
				return new FiniteFieldValue((FiniteFieldDomain)domain);
			}
			@SuppressWarnings("unchecked")
			TypedDiscreteDomain<Integer> intDomain = (TypedDiscreteDomain<Integer>) domain;
			return new GenericIntDiscreteValue(intDomain);
		}
		else if (Double.class.isAssignableFrom(domain.getElementClass()))
		{
			if (domain instanceof DoubleRangeDomain)
			{
				DoubleRangeDomain rangeDomain = (DoubleRangeDomain)domain;
				if (rangeDomain.getLowerBound() == 0.0 && rangeDomain.getInterval() == 1.0)
				{
					return new SimpleDoubleRangeValue(rangeDomain);
				}
				else
				{
					return new DoubleRangeValue(rangeDomain);
				}
			}
			@SuppressWarnings("unchecked")
			TypedDiscreteDomain<Double> doubleDomain = (TypedDiscreteDomain<Double>) domain;
			return new GenericDoubleDiscreteValue(doubleDomain);
		}
		else
		{
			return new GenericDiscreteValue(domain.asDiscrete());
		}
	}
	
	/**
	 * Creates a {@link FiniteFieldValue} instance for given {@code domain}.
	 * @since 0.07
	 */
	public static FiniteFieldValue create(FiniteFieldDomain domain)
	{
		return new FiniteFieldValue(domain);
	}
	
	/**
	 * Creates a {@code Value} instance for given {@code domain} with specified initial {@code value}.
	 * <p>
	 * Simply calls {@link Value#setObject(Object)} on instance returned by {@link #create(Domain)}.
	 */
	public static Value create(Domain domain, @Nullable Object value)
	{
		Value instance = create(domain);
		instance.setObject(value);
		return instance;
	}
	
	/**
	 * Creates a {@link DiscreteValue} instance for given {@code domain} with specified initial {@code value}.
	 * <p>
	 * Simply calls {@link Value#setObject(Object)} on instance returned by {@link #create(DiscreteDomain)}.
	 * @see #createWithIndex(DiscreteDomain, int)
	 */
	public static DiscreteValue create(DiscreteDomain domain, Object value)
	{
		DiscreteValue discrete = create(domain);
		discrete.setObject(value);
		return discrete;
	}
	
	/**
	 * Creates a {@link RealValue} instance for given {@code domain} with specified initial {@code value}.
	 */
	public static RealValue create(RealDomain domain, double value)
	{
		// TODO: use domain
		return new RealValue(value);
	}

	/**
	 * Creates a {@link RealJointValue} instance for given {@code domain} with specified initial {@code value}.
	 */
	public static RealJointValue create(RealJointDomain domain, double[] value)
	{
		// TODO: use domain
		return new RealJointValue(value);
	}
	
	/**
	 * Creates a {@link RealValue} with unbounded domain and initial value matching {@code value}.
	 * @since 0.08
	 */
	public static RealValue createReal(double value)
	{
		return new RealValue(value);
	}
	
	/**
	 * Creates a {@link RealJointValue} with unbounded domain and dimensions and initial value matching {@code value}.
	 * @since 0.08
	 */
	public static RealJointValue createRealJoint(double ... value)
	{
		return new RealJointValue(value);
	}
	
	/**
	 * Creates a new {@link DiscreteValue} instance for given {@code domain} with value specified by {@code index}.
	 * @param domain is the domain of the new value.
	 * @param index is an index into the domain specifying the value's initial value.
	 * @throws IndexOutOfBoundsException if {@code index} is not in range.
	 * @since 0.08
	 */
	public static DiscreteValue createWithIndex(DiscreteDomain domain, int index)
	{
		DiscreteValue discrete = create(domain);
		discrete.setIndex(index);
		return discrete;
	}
	
	/**
	 * Creates a {@code Value} instance with specified {@code initialValue}.
	 */
	public static Value create(@Nullable Object initialValue)
	{
		if (initialValue instanceof Number)
		{
			Number number = (Number)initialValue;
			if (number instanceof Integer || number instanceof Short || number instanceof Byte)
			{
				return new IntValue(number.intValue());
			}
			else if (number instanceof FiniteFieldNumber)
			{
				return new FiniteFieldValue((FiniteFieldNumber)number);
			}
			else
			{
				return new RealValue(number.doubleValue());
			}
		}
		else if (initialValue instanceof double[])
		{
			return new RealJointValue((double[])initialValue);
		}
		else if (initialValue instanceof Boolean)
		{
			return createWithIndex(DiscreteDomain.bool(), (Boolean)initialValue ? 1 : 0);
		}
		else
		{
			return new ObjectValue(initialValue);
		}
	}
	
	/**
	 * Returns a copy of the object.
	 * <p>
	 * Note that if {@link #isMutable()} is false, this will return an immutable
	 * object (and may in fact return the same object). If you need a mutable copy
	 * then instead use {@link #mutableClone()}.
	 */
	@Override
	public abstract Value clone();
	
	/**
	 * Returns an immutable clone of the value.
	 * <p>
	 * If the value is already immutable it will simply be returned, otherwise this
	 * will return a newly created immutable value.
	 * <p>
	 * @since 0.08
	 */
	public abstract Value immutableClone();
	
	/**
	 * Returns a mutable clone of the value.
	 * <p>
	 * This returns a {@linkplain #isMutable() mutable} copy of the value.
	 * <p>
	 * @since 0.08
	 */
	public Value mutableClone()
	{
		return clone();
	}
	
	/*----------------
	 * Object methods
	 */
	
	@Override
	public String toString()
	{
		return getObject() + "";
	}

	/*-----------------
	 * IEquals methods
	 */
	
	@Override
	public boolean objectEquals(@Nullable Object other)
	{
		if (this == other)
		{
			return true;
		}
		
		if (other instanceof Value)
		{
			return valueEquals((Value)other);
		}
		
		return false;
	}

	/*----------------
	 * IDatum methods
	 */
	
	/**
	 * {@inheritDoc}
	 * <p>
	 * Simply returns 0.0 if passed in {@code value} {@link #valueEquals} this value, and infinity otherwise.
	 */
	@Override
	public double evalEnergy(Value value)
	{
		return valueEquals(value) ? 0.0 : Double.POSITIVE_INFINITY;
	}
	
	/**
	 * {@inheritDoc}
	 * <p>
	 * Returns {@link DataRepresentationType#VALUE}.
	 */
	@Override
	public final DataRepresentationType representationType()
	{
		return DataRepresentationType.VALUE;
	}
	
	/*---------------
	 * Value methods
	 */
	
	/**
	 * True if the value may be changed after construction.
	 * <p>
	 * If false, then {@link #setObject} and all other {@code set*} methods
	 * will throw a {@link UnsupportedOperationException}.
	 * <p>
	 * The default implementation returns true.
	 * @since 0.08
	 */
	public boolean isMutable()
	{
		return true;
	}
	
	/**
	 * Domain for valid contents of value object.
	 */
	public abstract Domain getDomain();
	
	/**
	 * Returns current value as a {@link Object}.
	 * <p>
	 * This may create a new object if underlying representation is a primitive type such as {@code int} or
	 * {@code double}; in such cases it is preferable to use {@link #getInt()}, {@link #getDouble()}, etc.
	 */
	public abstract @Nullable Object getObject();
	
	/**
	 * Sets current value from an {@link Object}.
	 */
	public abstract void setObject(@Nullable Object value);
	
	/**
	 * Returns current value as a {@code boolean}.
	 */
	public boolean getBoolean()
	{
		return FactorFunctionUtilities.toBoolean(getObject());
	}
	
	/**
	 * Gets current value as a {@code double}.
	 */
	public double getDouble()
	{
		return FactorFunctionUtilities.toDouble(getObject());
	}
	
	public double[] getDoubleArray()
	{
		Object val = getObject();
		if (val instanceof double[])
		{
			return (double[])val;
		}
		return new double[] { getDouble() };
	}
	
	/**
	 * Sets current value from a {@code double}.
	 */
	public void setDouble(double value)
	{
		setObject(value);
	}
	
	
	/**
	 * Gets the current value as a {@code FiniteFieldNumber}
	 * @since 0.07
	 * @throws ClassCastException if value does not contain a finite field value
	 * @throws NullPointerException if {@link #getObject()} is null.
	 */
	public FiniteFieldNumber getFiniteField()
	{
		return (FiniteFieldNumber)requireNonNull(getObject());
	}
	
	/**
	 * Sets the current value from a {@code FiniteFieldNumber}
	 * @since 0.07
	 */
	public void setFiniteField(FiniteFieldNumber value)
	{
		setObject(value);
	}

	
	/**
	 * If value is known to be a member of a {@link DiscreteDomain}, returns its index within its domain otherwise -1.
	 */
	public int getIndex()
	{
		return -1;
	}

	/**
	 * Returns either discrete index or integer value.
	 * <p>
	 * Same as {@link #getIndex()} if this is a {@link DiscreteValue} and otherwise is the same as {@link #getInt()}.
	 * @since 0.08
	 */
	public int getIndexOrInt()
	{
		return getInt();
	}
	
	/**
	 * Returns current value as a {@code int}.
	 */
	public int getInt()
	{
		return FactorFunctionUtilities.toInteger(getObject());
	}
	
	/**
	 * Sets contents from another value.
	 * <p>
	 * Default implementation uses {@link #getObject()}/{@link #setObject(Object)}. Subclasses implement
	 * this more efficiently to avoid {@link Object} conversion.
	 */
	public void setFrom(Value value)
	{
		setObject(value.getObject());
	}
	
	/**
	 * If value is known to be a member of a {@link DiscreteDomain}, sets value to element with
	 * given index within the domain.
	 * @throws UnsupportedOperationException if not a {@link DiscreteValue}
	 * @throws IndexOutOfBoundsException if index is negative or greater than or equal to the domain size.
	 */
	public void setIndex(int index)
	{
		throw new UnsupportedOperationException("setIndex only supported on DiscreteValue");
	}
	
	/**
	 * Sets contents from a {@code int}.
	 */
	public void setInt(int value)
	{
		setObject(value);
	}
	
	/**
	 * Sets contents from a {@code boolean}
	 * Subclasses should override this to set a boolean in the appropriate form
	 * @since 0.07
	 */
	public void setBoolean(boolean value)
	{
		setObject(value);
	}
	
	/**
	 * True if {@link #setFrom(Value)} on {@code other} would not change the state of this value.
	 * <p>
	 * Default implementation compares {@link #getObject()} values.
	 */
	public boolean valueEquals(Value other)
	{
		return Objects.equals(getObject(), other.getObject());
	}

	/*----------
	 * Internal
	 */
	
	/**
	 * Returns an exception indicating that value is not mutable.
	 * @since 0.08
	 */
	protected RuntimeException notMutable()
	{
		throw new UnsupportedOperationException("Value is not mutable");
	}

	/*-----------------------
	 * Static helper methods
	 */
	
	/**
	 * Fills in and returns {@code Value} array from array of objects.
	 * 
	 * @param objs must have values that are compatible with members of output array.
	 * @param output must be the same length as object array and contain concrete {@link Value}
	 * instances compatible with corresponding objects.
	 * @return {@code output} argument
	 * 
	 * @see #createFromObjects(Object[], Class, Domain...)
	 */
	public static <T extends Value> T[] copyFromObjects(Object[] objs, T[] output)
	{
		for (int i = objs.length; --i>=0;)
		{
			output[i].setObject(objs[i]);
		}
		return output;
	}
	
	/**
	 * Creates {@code Value} array from array of objects using declared subclass type and provided
	 * domains.
	 * 
	 * @param objs
	 * @param valueClass specifies the component type of the the returned array.
	 * @param domains specifies the domains of the objects. This must have between 1 and {@code objs.length}
	 * elements. If less than the maximum, the last domain will be applied to all remaining objects.
	 * @return array of {@code Value} instances containing the corresponding values from the object array.
	 * 
	 * @see #createFromObjects(Object[], Domain...)
	 * @see #copyFromObjects(Object[], Value[])
	 */
	public static <T extends Value> T[] createFromObjects(Object[] objs, Class<T> valueClass, Domain ... domains)
	{
		final int size = objs.length;
		final int nDomains = domains.length;
		assert(nDomains > 0 && nDomains <= size);
		
		@SuppressWarnings("unchecked")
		final T[] output = (T[]) Array.newInstance(valueClass, size);
		for (int i = 0; i < size; ++i)
		{
			final Domain domain = i < nDomains ? domains[i] : domains[nDomains - 1];
			
			@SuppressWarnings("unchecked")
			T value = (T) create(domain, objs[i]);
			output[i] = value;
		}
		return output;
	}
	
	/**
	 * Same as {@link #createFromObjects(Object[], Class, Domain...)}
	 */
	public static Value[] createFromObjects(Object[] objs, Domain ... domains)
	{
		return createFromObjects(objs, Value.class, domains);
	}

	/**
	 * Creates {@code Value} array with empty or default content from array of Domains.
	 * 
	 * @param domains specifies the domains of the objects. This must have between 1 and {@code objs.length}
	 * elements. If less than the maximum, the last domain will be applied to all remaining objects.
	 * @return array of {@code Value} instances each with the corresponding domain.
	 * @since 0.07
	 */
	public static Value[] createFromDomains(Domain... domains)
	{
		final int nDomains = domains.length;
		
		final Value[] output = new Value[nDomains];
		for (int i = 0; i < nDomains; ++i)
		{
			Value value = create(domains[i]);
			output[i] = value;
		}
		return output;
	}

	/**
	 * Creates {@code Value} array with empty or default content from a DomainList.
	 * 
	 * @param domains specifies the domains of the objects. This must have between 1 and {@code objs.length}
	 * elements. If less than the maximum, the last domain will be applied to all remaining objects.
	 * @return array of {@code Value} instances each with the corresponding domain.
	 * @since 0.07
	 */
	public static Value[] createFromDomains(DomainList<?> domains)
	{
		final int nDomains = domains.size();
		
		final Value[] output = new Value[nDomains];
		for (int i = 0; i < nDomains; ++i)
		{
			Value value = create(domains.get(i));
			output[i] = value;
		}
		return output;
	}

	/**
	 * Copies current contents of {@code values} into {@code output} array.
	 */
	public static <T extends Value> Object[] toObjects(T[] values, Object[] output)
	{
		for (int i = values.length; --i>=0;)
		{
			output[i] = values[i].getObject();
		}
		return output;
	}
	
	/**
	 * Converts an array of {@code Value} to an array of {@link Object} containing the current
	 * contents of the values.
	 */
	public static <T extends Value> Object[] toObjects(T[] values)
	{
		return toObjects(values, new Object[values.length]);
	}
}
