/*******************************************************************************
*   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.factorfunctions.core;

import java.util.BitSet;
import java.util.Random;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.dimple.exceptions.DimpleException;
import com.analog.lyric.dimple.model.domains.DiscreteDomain;
import com.analog.lyric.dimple.model.domains.JointDiscreteDomain;
import com.analog.lyric.dimple.model.domains.JointDomainIndexer;
import com.analog.lyric.dimple.model.domains.JointDomainReindexer;
import com.analog.lyric.dimple.model.values.Value;

public interface IFactorTable extends IFactorTableBase
{
	@Override
	public IFactorTable clone();
	
	/**
	 * Removes entries from sparse representation of table that have zero weights.
	 * <p>
	 * This may affect the {@link #sparseSize()} and the relationship between sparse and
	 * joint indexes.
	 * <p>
	 * @return the number of sparse entries that were removed.
	 */
	public int compact();

	@Override
	public IFactorTable convert(JointDomainReindexer converter);
	
	public void copy(IFactorTable that);

	/**
	 * Constructs a new factor table by conditioning one or more dimensions
	 * conditioned on specified values.
	 * <p>
	 * The new table will not be directed and will have the same representation as the original
	 * table except that {@link FactorTableRepresentation#DETERMINISTIC} will become
	 * {@link FactorTableRepresentation#SPARSE_ENERGY}.
	 * <p>
	 * @param valueIndices is an array of length {@link #getDimensions()} that specifies which
	 * dimensions are to be conditioned away. Each entry in the array should either be
	 * a negative value if the dimension is to be retained, or a non-negative value in
	 * the range [0, <i>dimension-size</i> - 1].
	 * 
	 * @since 0.05
	 * @see #createTableConditionedOn(int[], boolean)
	 */
	public IFactorTable createTableConditionedOn(int[] valueIndices);

	/**
	 * Constructs a new factor table by conditioning one or more dimensions
	 * conditioned on specified values.
	 * <p>
	 * The new table will only retain its directedness if {@code retainDimensions} is true
	 * and will have the same representation as the original
	 * table except that {@link FactorTableRepresentation#DETERMINISTIC} will become
	 * {@link FactorTableRepresentation#SPARSE_ENERGY}.
	 * <p>
	 * @param valueIndices is an array of length {@link #getDimensions()} that specifies which
	 * dimensions are to be conditioned away. Each entry in the array should either be
	 * a negative value if the dimension is to be retained, or a non-negative value in
	 * the range [0, <i>dimension-size</i> - 1].
	 * @param retainDimensions if true, the new table will retain the same number of dimensions as
	 * the original table, but the domains of the conditioned dimensions will become single-element
	 * domains. Otherwise conditioned dimensions will be removed from the new table entirely.
	 * <p>
	 * @since 0.08
	 * @see #createTableConditionedOn(int[])
	 */
	public IFactorTable createTableConditionedOn(int[] valueIndices, boolean retainDimensions);
	
	/**
	 * Constructs a new factor table by appending dimensions for the specified {@code newDomains}.
	 */
	public IFactorTable createTableWithNewVariables(DiscreteDomain[] newDomains);

	/**
	 * Returns an enum indicating the underlying representation of the factor table.
	 * <p>
	 * The representation may be changed via {@link #setRepresentation(FactorTableRepresentation)} but
	 * can also be changed implicitly as a side effect of certain other methods:
	 * <ul>
	 * <li>{@link #getEnergiesSparseUnsafe()}
	 * <li>{@link #getEnergyForSparseIndex(int)}
	 * <li>{@link #getIndicesSparseUnsafe()}
	 * <li>{@link #getWeightForSparseIndex(int)}
	 * <li>{@link #getWeightsSparseUnsafe()}
	 * <li>{@link #setEnergyForJointIndex(double, int)}
	 * <li>{@link #setEnergyForSparseIndex(double, int)}
	 * <li>{@link #setWeightForJointIndex(double, int)}
	 * <li>{@link #setWeightForSparseIndex(double, int)}
	 * <li>{@link #sparseIndexFromJointIndex(int)}
	 * <li>{@link #sparseIndexToJointIndex(int)}
	 * </ul>
	 */
	public FactorTableRepresentation getRepresentation();

	/**
	 * Creates a new factor table that replaces two or more of its domains with
	 * a joint domain.
	 * <p>
	 * @param varIndices contains the indices of the domains to be joined. Must have at least two entries
	 * in the range [0,{@link #getDimensions()}-1].
	 * @param indexToJointIndex specifies the order in which the joined domains are to be incorporated
	 * into the new joint domain.
	 * @param allDomains is the list of all domains before joining
	 * @param jointDomain is the new joined domain. Its size must match the product of the sizes of the
	 * joined domains, and it is expected to be of type {@link JointDiscreteDomain}. It will be the
	 * last domain in the new table's domain list.
	 */
	// Only used by DiscreteFactor.replaceVariablesWithJoint
	public IFactorTable joinVariablesAndCreateNewTable(
		int [] varIndices,
		int [] indexToJointIndex,
		DiscreteDomain [] allDomains,
		DiscreteDomain jointDomain); // REFACTOR: keep (for now)

	/**
	 * True if table currently has a deterministic directed representation (either
	 * {@link FactorTableRepresentation#DETERMINISTIC} or {@link FactorTableRepresentation#DETERMINISTIC_WITH_INDICES}).
	 * Unlike invoking {@link #isDeterministicDirected()}, this will not check the values of the table
	 * or change the representation.
	 */
	public boolean hasDeterministicRepresentation();
	
	/**
	 * Returns the underlying array of sparse energies without copying for speed.
	 * <p>
	 * <b>IMPORTANT</b>: modifying the contents of the array may put the factor table into
	 * an invalid state. This should be treated as a read-only value.
	 * <p>
	 * If necessary, this method will implicitly modify the representation to include sparse energies
	 * (@link {@link #hasSparseEnergies()}).
	 * <p>
	 * @see #getWeightsSparseUnsafe()
	 */
	public double[] getEnergiesSparseUnsafe();
	
	/**
	 * Returns the underlying array of dense energies without copying for speed.
	 * <p>
	 * <b>IMPORTANT</b>: modifying the contents of the array may put the factor table into
	 * an invalid state. This should be treated as a read-only value.
	 * <p>
	 * If necessary, this method will implicitly modify the representation to include dense weights
	 * <p>
	 * @since 0.07
	 */
	public double[] getEnergiesDenseUnsafe();

	/**
	 * Returns an array of energies for the {@code sliceDimension} of the factor table with all other
	 * dimensions fixed to provided values.
	 * <p>
	 * Same as {@link #getEnergySlice(double[], int, int[])} but always allocates a new array.
	 * <p>
	 * @see #getWeightSlice(int, int[])
	 */
	public double[] getEnergySlice(int sliceDimension, int ... indices);
	
	/**
	 * Returns an array of energies for the {@code sliceDimension} of the factor table with all other
	 * dimensions fixed to provided values.
	 * <p>
	 * Same as {@link #getEnergySlice(int, int[])} but always allocates a new array.
	 * <p>
	 * @see #getWeightSlice(int, Value...)
	 */
	public double[] getEnergySlice(int sliceDimension, Value ... values);
	
	/**
	 * Returns an array of energies for the {@code sliceDimension} of the factor table with all other
	 * dimensions fixed to provided values.
	 * 
	 * @param sliceDimension is an integer in the range [0, {@link #getDimensions()}-1] that identifies which
	 * domain the slice is for.
	 * @param indices specifies the element indices that are to be fixed. The element index at position
	 * {@code sliceDimension} will be ignored.
	 * @param slice if non-null and large enough to accommodate the values will be used as the return value,
	 * otherwise a new array will be allocated.
	 * @return an array of energy values with size equal to the size of the {@code sliceDimension} of
	 * the table (i.e. {@code getDomainIndexer().getDomainSize(sliceDimension)}) holding the energy values
	 * from the table
	 * <p>
	 * @see #getEnergySlice(int, int[])
	 * @see #getWeightSlice(double[], int, int[])
	 */
	public double[] getEnergySlice(@Nullable double[] slice, int sliceDimension, int ... indices);
	
	/**
	 * Returns an array of energies for the {@code sliceDimension} of the factor table with all other
	 * dimensions fixed to provided values.
	 * 
	 * @param sliceDimension is an integer in the range [0, {@link #getDimensions()}-1] that identifies which
	 * domain the slice is for.
	 * @param values specifies the values of the fixed dimensions. The element value at position
	 * {@code sliceDimension} will be ignored.
	 * @param slice if non-null and large enough to accommodate the values will be used as the return value,
	 * otherwise a new array will be allocated.
	 * @return an array of energy values with size equal to the size of the {@code sliceDimension} of
	 * the table (i.e. {@code getDomainIndexer().getDomainSize(sliceDimension)}) holding the energy values
	 * from the table
	 * <p>
	 * @see #getEnergySlice(double[], int, int[])
	 * @see #getWeightSlice(double[], int, Value...)
	 */
	public double[] getEnergySlice(@Nullable double[] slice, int sliceDimension, Value ... values);
	
	/**
	 * Get factor function used to populate the table, if any.
	 * <p>
	 * This will be the function provided to the last call of {@link #populateFromFunction(FactorFunction)},
	 * if any. The function may also be cleared subsequent calls that replace all of the table entries
	 * (e.g. {@link #setEnergiesDense(double[])}.
	 * @since 0.08
	 * @see #populateFromFunction(FactorFunction)
	 */
	public @Nullable FactorFunction getFactorFunction();
	
	/**
	 * Returns the underlying array of sparse element indices.
	 * <p>
	 * <b>IMPORTANT</b>: modifying the contents of the array may put the factor table into
	 * an invalid state. This should be treated as a read-only value.
	 * <p>
	 * If necessary, this method will implicitly modify the representation to include sparse indices
	 * (see {@link #hasSparseIndices()}) and if the table did not previously only had a dense value
	 * representation (see {@link #hasDenseRepresentation()}) it will implicitly add a sparse
	 * representation introducing sparse weights if the table had dense weights and otherwise introducing
	 * sparse energies.
	 */
	public int[][] getIndicesSparseUnsafe();
	
	/**
	 * {@inheritDoc}
	 * <p>
	 * If necessary, this method will implicitly modify the representation to include a sparse
	 * form if it previously only held dense values; in this case, if the table only held dense energies, then
	 * sparse energies will be added, otherwise sparse weights will be added.
	 */
	@Override
	public double getWeightForSparseIndex(int sparseIndex);
	
	/**
	 * Returns the underlying array of sparse weights without copying for speed.
	 * <p>
	 * <b>IMPORTANT</b>: modifying the contents of the array may put the factor table into
	 * an invalid state. This should be treated as a read-only value.
	 * <p>
	 * If necessary, this method will implicitly modify the representation to include sparse weights
	 * (@link {@link #hasSparseEnergies()}).
	 * <p>
	 * @see #getEnergiesSparseUnsafe()
	 */
	public double[] getWeightsSparseUnsafe();

	/**
	 * Returns the underlying array of dense weights without copying for speed.
	 * <p>
	 * <b>IMPORTANT</b>: modifying the contents of the array may put the factor table into
	 * an invalid state. This should be treated as a read-only value.
	 * <p>
	 * If necessary, this method will implicitly modify the representation to include dense weights
	 * <p>
	 * @since 0.06
	 */
	public double[] getWeightsDenseUnsafe();
	
	/**
	 * Returns an array of weights for the {@code sliceDimension} of the factor table with all other
	 * dimensions fixed to provided values.
	 * <p>
	 * Same as {@link #getWeightSlice(double[], int, int[])} but always allocates a new array.
	 * <p>
	 * @see #getEnergySlice(int, int[])
	 */
	public double[] getWeightSlice(int sliceDimension, int ... indices);
	
	/**
	 * Returns an array of weights for the {@code sliceDimension} of the factor table with all other
	 * dimensions fixed to provided values.
	 * <p>
	 * Same as {@link #getWeightSlice(int, Value[])} but always allocates a new array.
	 * <p>
	 * @see #getEnergySlice(int, Value[])
	 */
	public double[] getWeightSlice(int sliceDimension, Value ... values);
	
	/**
	 * Returns an array of weights for the {@code sliceDimension} of the factor table with all other
	 * dimensions fixed to provided values.
	 * 
	 * @param sliceDimension is an integer in the range [0, {@link #getDimensions()}-1] that identifies which
	 * domain the slice is for.
	 * @param indices specifies the element indices that are to be fixed. The element index at position
	 * {@code sliceDimension} will be ignored.
	 * @param slice if non-null and large enough to accommodate the values will be used as the return value,
	 * otherwise a new array will be allocated.
	 * @return an array of weight values with size equal to the size of the {@code sliceDimension} of
	 * the table (i.e. {@code getDomainIndexer().getDomainSize(sliceDimension)}) holding the weight values
	 * from the table
	 * <p>
	 * @see #getWeightSlice(int, int[])
	 * @see #getEnergySlice(double[], int, int[])
	 */
	public double[] getWeightSlice(@Nullable double[] slice, int sliceDimension, int ... indices);
	
	/**
	 * Returns an array of weights for the {@code sliceDimension} of the factor table with all other
	 * dimensions fixed to provided values.
	 * 
	 * @param sliceDimension is an integer in the range [0, {@link #getDimensions()}-1] that identifies which
	 * domain the slice is for.
	 * @param values specifies the values of the fixed dimensions. The element value at position
	 * {@code sliceDimension} will be ignored.
	 * @param slice if non-null and large enough to accommodate the values will be used as the return value,
	 * otherwise a new array will be allocated.
	 * @return an array of weight values with size equal to the size of the {@code sliceDimension} of
	 * the table (i.e. {@code getDomainIndexer().getDomainSize(sliceDimension)}) holding the weight values
	 * from the table
	 * <p>
	 * @see #getWeightSlice(int, Value[])
	 * @see #getEnergySlice(double[], int, Value[])
	 */
	public double[] getWeightSlice(@Nullable double[] slice, int sliceDimension, Value ... values);
	
	/**
	 * True if the current factor table representation supports {@link #getIndicesSparseUnsafe}.
	 */
	public boolean hasSparseIndices();
	
	/**
	 * Populates entries of table from given factor function.
	 * @param function
	 * @since 0.08
	 * @see #getFactorFunction()
	 */
	public void populateFromFunction(FactorFunction function);
	
	/**
	 * Replace the energies/weights of the table from the given array of {@code energies} in
	 * order of sparse index. This does not alter the sparse to joint index mapping.
	 * <p>
	 * @see #replaceWeightsSparse(double[])
	 */
	public void replaceEnergiesSparse(double[] energies);
	
	/**
	 * Replace the energies/weights of the table from the given array of {@code weights} in
	 * order of sparse index. This does not alter the sparse to joint index mapping.
	 * <p>
	 * @see #replaceEnergiesSparse(double[])
	 */
	public void replaceWeightsSparse(double[] weights);
	
	/**
	 * Sets representation to {@link FactorTableRepresentation#DENSE_ENERGY} with
	 * provided energies replacing the existing contents of the table.
	 * @param energies specifies the energies of the table in dense joint-index order. Must have length
	 * equal to {@link #getDomainIndexer()}.getCardinality().
	 */
	public void setEnergiesDense(double[] energies);
	
	/**
	 * Sets representation to {@link FactorTableRepresentation#DENSE_WEIGHT} with
	 * provided weights replacing the existing contents of the table.
	 * @param weights specifies the weights of the table in dense joint-index order. Must have length
	 * equal to {@link #getDomainIndexer()}.getCardinality().
	 */
	public void setWeightsDense(double[] weights);

	/**
	 * Makes table directed and verifies that its weights are normalized correctly.
	 * <p>
	 * Similar to {@link #setDirected(BitSet)} but does not allow a null argument and
	 * will throw an exception if not {@link #isConditional()}.
	 */
	public void setConditional(BitSet outputSet);
	
	/**
	 * Makes table directed and normalizes its weights to conditional form if necessary.
	 * <p>
	 * Equivalent to calling {@link #setDirected(BitSet)} followed by {@link #normalizeConditional()}
	 * but requires that {@code outputSet} is non-null.
	 */
	public void makeConditional(BitSet outputSet);

	/**
	 * Randomize the weights of the table depending on underlying representation.
	 * <p>
	 * If table {@link #hasDenseRepresentation()} this will randomly assign weights/energies
	 * to entries for all joint indexes, otherwise it will only assign values for all sparse
	 * indexes. Weights will be assigned uniformly from the range (0,1].
	 */
	public void randomizeWeights(Random rand);

	/**
	 * Sets representation to {@link FactorTableRepresentation#DETERMINISTIC} with given set of
	 * outputs.
	 * <p>
	 * @param outputIndices any array mapping input indices, representing the joint value of all input
	 * values, to output indices, representing the joint value of all outputs. The length of the array
	 * must be equal to the value of {@link JointDomainIndexer#getInputCardinality()} on {@link #getDomainIndexer()}.
	 * @throws UnsupportedOperationException if not {@link #isDirected()}.
	 */
	public void setDeterministicOutputIndices(int[] outputIndices);

	/**
	 * Designates directionality of factor table by specifying which of its domains are outputs.
	 * <p>
	 * Note that this may change the indexing of the table's values in both sparse and dense formats.
	 * You can only rely on the indexing not to change if all output domains are at the front of the
	 * domain list (i.e. {@link JointDomainIndexer#hasCanonicalDomainOrder()}).
	 * <p>
	 * @param outputSet if null, makes the table undirected otherwise this should turn on bits corresponding
	 * to the domains that are to be designated as outputs. The highest bit must be less than the number of
	 * domains ({@link #getDimensions()}).
	 */
	public void setDirected(@Nullable BitSet outputSet);
	
	/**
	 * {@inheritDoc}
	 * <p>
	 * If the representation started out as deterministic ({@link #hasDeterministicRepresentation()})
	 * and the energy changed, it will be
	 * implicitly converted to {@link FactorTableRepresentation#DENSE_ENERGY} and if
	 * {@link FactorTableRepresentation#DETERMINISTIC_WITH_INDICES} it will be converted to
	 * {@link FactorTableRepresentation#ALL_ENERGY_WITH_INDICES}.
	 */
	@Override
	public void setEnergyForJointIndex(double energy, int jointIndex);
	
	/**
	 * {@inheritDoc}
	 * <p>
	 * If the representation started out as deterministic ({@link #hasDeterministicRepresentation()})
	 * and the energy changed, it will be converted to sparse energies (retaining sparse indices if they exist).
	 */
	@Override
	public void setEnergyForSparseIndex(double energy, int sparseIndex);
	
	/**
	 * {@inheritDoc}
	 * <p>
	 * If the representation started out as {@link FactorTableRepresentation#DETERMINISTIC}, it will be
	 * and the weight changed, it will be implicitly converted to {@link FactorTableRepresentation#DENSE_WEIGHT} and if
	 * {@link FactorTableRepresentation#DETERMINISTIC_WITH_INDICES} it will be converted to
	 * {@link FactorTableRepresentation#ALL_WEIGHT_WITH_INDICES}.
	 */
	@Override
	public void setWeightForJointIndex(double weight, int jointIndex);
	
	/**
	 * {@inheritDoc}
	 * <p>
	 * If the representation started out as deterministic ({@link #hasDeterministicRepresentation()})
	 * and the weight changed, it will be converted to sparse weights (retaining sparse indices if they exist).
	 */
	@Override
	public void setWeightForSparseIndex(double weight, int sparseIndex);

	/**
	 * Sets the underlying representation of the table to the specified value.
	 * <p>
	 * @throws DimpleException If setting representation to a deterministic representation, this method will throw an
	 * exception if the table cannot be represented as deterministic (see {@link #isDeterministicDirected()}) or
	 * table does not support joint indexing and a dense representation is requested
	 * (see {@link #supportsJointIndexing()}.
	 */
	public void setRepresentation(FactorTableRepresentation representation);
	
	/**
	 * Sets representation to {@link FactorTableRepresentation#SPARSE_ENERGY} with
	 * provided energies for each joint index.
	 * <p>
	 * @param jointIndices are the joint indexes of the entries to put in the table.
	 * @param energies specifies the energies of the table in the same order as {@code jointIndices}.
	 * @throws IllegalArgumentException if {@code jointIndices} and {@code energies} have different lengths,
	 * if there are duplicate indices or any of the indices is not in a valid range for the table.
	 * @see #setEnergiesSparse(int[][], double[])
	 * @see #setWeightsSparse(int[], double[])
	 */
	public void setEnergiesSparse(int[] jointIndices, double[] energies);
	
	/**
	 * Sets representation to {@link FactorTableRepresentation#SPARSE_ENERGY} with
	 * provided weights for each set of indices.
	 * <p>
	 * @param indices are the combined element indices of the entries to put in the table.
	 * @param energies specifies the energies of the table in the same order as {@code indices}.
	 * @throws IllegalArgumentException if {@code indices} and {@code energies} have different lengths,
	 * if there are duplicate indices or any of the indices is not in a valid range for the table.
	 * @see #setEnergiesSparse(int[], double[])
	 * @see #setWeightsSparse(int[][], double[])
	 * @since 0.05
	 */
	public void setEnergiesSparse(int[][] indices, double[] energies);

	/**
	 * Sets representation to {@link FactorTableRepresentation#SPARSE_WEIGHT} with
	 * provided weights for each joint index.
	 * <p>
	 * @param jointIndices are the joint indexes of the entries to put in the table.
	 * @param weights specifies the weights of the table in the same order as {@code jointIndices}.
	 * @throws IllegalArgumentException if {@code jointIndices} and {@code weights} have different lengths,
	 * if there are duplicate indices or any of the indices is not in a valid range for the table.
	 * @see #setWeightsSparse(int[][], double[])
	 * @see #setEnergiesSparse(int[], double[])
	 */
	public void setWeightsSparse(int[] jointIndices, double[] weights);
	
	/**
	 * Sets representation to {@link FactorTableRepresentation#SPARSE_WEIGHT} with
	 * provided weights for each set of indices.
	 * <p>
	 * @param indices are the combined element indices of the entries to put in the table.
	 * @param weights specifies the weights of the table in the same order as {@code indices}.
	 * @throws IllegalArgumentException if {@code indices} and {@code weights} have different lengths,
	 * if there are duplicate indices or any of the indices is not in a valid range for the table.
	 * @see #setWeightsSparse(int[], double[])
	 * @see #setEnergiesSparse(int[][], double[])
	 */
	public void setWeightsSparse(int[][] indices, double[] weights);
}
