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

import java.util.AbstractList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.RandomAccess;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.dimple.events.IModelEventSource;
import com.analog.lyric.dimple.model.core.FactorGraph;
import com.analog.lyric.dimple.model.core.FactorGraphChild;
import com.analog.lyric.dimple.model.core.Ids;
import com.analog.lyric.util.misc.Internal;
import com.google.common.primitives.Longs;

import cern.colt.map.OpenLongObjectHashMap;
import net.jcip.annotations.ThreadSafe;

/**
 * Represents a block of {@link Variable}s in a {@link FactorGraph}.
 * <p>
 * The list of variables in this block is immutable and cannot change once it has been created.
 * <p>
 * Note that while the list of variables cannot change, the underlying representation can change if
 * the {@linkplain FactorGraph#reindexGraphTree graph tree is reindexed}. This may change the value
 * of the {@link #hashCode()}, so avoid reindexing when you have {@link java.util.HashSet HashSet}
 * containing blocks from the graph tree, or a {@link java.util.HashMap HashMap} with blocks used
 * as keys.
 * <p>
 * @since 0.08
 * @author Christopher Barber
 * @see FactorGraph#addVariableBlock(Collection)
 */
@ThreadSafe
public final class VariableBlock extends FactorGraphChild implements List<Variable>, RandomAccess
{
	/*-------
	 * State
	 */
	
	private final long[] _variableGraphTreeIds;
	private int _hashCode;
	
	/*--------------
	 * Construction
	 */
	
	/**
	 * Construct a new variable block.
	 * <p>
	 * This constructor is intended to be used internally in the implementation of
	 * {@link FactorGraph#addVariableBlock(Collection)}.
	 * <p>
	 * @param parent is the graph
	 * @param variables are the variables that will comprise the block. The variables will be added in the
	 * order of the {@linkplain Collection#iterator iterator}.
	 * @since 0.08
	 * @throws IllegalArgumentException if a variable does not belong to the same tree of graphs as {@code parent}
	 * or the same variable appears more than once.
	 * @category internal
	 */
	@Internal
	public VariableBlock(FactorGraph parent, Collection<Variable> variables)
	{
		super();
		super.setParentGraph(parent);
		
		final int n = variables.size();
		
		if (n <= 0)
		{
			throw new IllegalArgumentException("Cannot create empty VariableBlock");
		}
		
		final OpenLongObjectHashMap varSet = new OpenLongObjectHashMap(n);
		final FactorGraph root = parent.getRootGraph();
		_variableGraphTreeIds = new long[n];
		
		int i = -1;
		for (Variable var : variables)
		{
			if (var.getRootGraph() != root)
			{
				throw new IllegalArgumentException(String.format("Variable '%s' not in graph tree", var));
			}
			
			// TODO - perhaps boundary variables should be stored using their boundary id
			final long id = var.getGraphTreeId();
			
			if (!varSet.put(id, null))
			{
				throw new IllegalArgumentException(String.format("Variable '%s' was specified more than once", var));
			}
			
			_variableGraphTreeIds[++i] = id;
		}
		
		_hashCode = Arrays.hashCode(_variableGraphTreeIds);
	}
	
	/*----------------
	 * Object methods
	 */

	@Override
	public boolean equals(@Nullable Object obj)
	{
		if (obj == this)
		{
			return true;
		}
		
		if (obj instanceof VariableBlock && obj.hashCode() == _hashCode)
		{
			return Arrays.equals(_variableGraphTreeIds, ((VariableBlock)obj)._variableGraphTreeIds);
		}
		
		return false;
	}
	
	@Override
	public int hashCode()
	{
		return _hashCode;
	}
	
	/*----------------------------
	 * IDimpleEventSource methods
	 */
	
	@Override
	public String getEventSourceName()
	{
		return toString();
	}

	@Override
	public @Nullable IModelEventSource getModelEventSource()
	{
		return getParentGraph();
	}

	@Override
	public void notifyListenerChanged()
	{
	}

	/*--------------------------
	 * FactorGraphChild methods
	 */
	
	@SuppressWarnings("null")
	@Override
	public FactorGraph getParentGraph()
	{
		return _parentGraph;
	}
	
	@Override
	protected void fixGraphTreeIndices(int[] old2newGraphTreeIndex)
	{
		final long[] ids = _variableGraphTreeIds;
		
		for (int i = ids.length; --i>=0;)
		{
			final long id = ids[i];
			final int oldGraphTreeIndex = Ids.graphTreeIndexFromGraphTreeId(id);
			final int newGraphTreeIndex = old2newGraphTreeIndex[oldGraphTreeIndex];
			ids[i] = Ids.graphTreeIdFromParts(newGraphTreeIndex, Ids.localIdFromGraphTreeId(id));
		}
		
		_hashCode = Arrays.hashCode(ids);
	}
	
	@Override
	protected void fixVarIndicesForGraph(int graphTreeIndex, int[] old2newVarIndex)
	{
		final long[] ids = _variableGraphTreeIds;
		
		for (int i = ids.length; --i>=0;)
		{
			final long id = ids[i];
			if (graphTreeIndex == Ids.graphTreeIndexFromGraphTreeId(id))
			{
				final int oldVarIndex = Ids.indexFromLocalId(Ids.localIdFromGraphTreeId(id));
				final int newVarIndex = old2newVarIndex[oldVarIndex];
				ids[i] = Ids.graphTreeIdFromParts(graphTreeIndex, Ids.localIdFromParts(Ids.VARIABLE_TYPE, newVarIndex));
			}
		}
		
		_hashCode = Arrays.hashCode(ids);
	}
	
	/*---------------
	 * List methods
	 */

	@Override
	public boolean isEmpty()
	{
		return false; // Empty variable block is not allowed, see constructor.
	}

	@Override
	public int size()
	{
		return _variableGraphTreeIds.length;
	}

	@NonNullByDefault(false)
	@Override
	public boolean contains(Object obj)
	{
		return indexOf(obj) >= 0;
	}

	@NonNullByDefault(false)
	@Override
	public boolean containsAll(Collection<?> collection)
	{
		for (Object obj : collection)
		{
			if (!contains(obj))
			{
				return false;
			}
		}
		
		return true;
	}

	@Override
	public Variable get(int index)
	{
		final long id = _variableGraphTreeIds[index];
		Variable var = (Variable)requireParentGraph().getNodeByGraphTreeId(id);
		
		if (var == null)
		{
			throw new IllegalStateException(String.format("Variable with graph tree id 0x%X no longer in graph", id));
		}
		
		return var;
	}

	@NonNullByDefault(false)
	@Override
	public int indexOf(Object obj)
	{
		if (obj instanceof Variable)
		{
			Variable var = (Variable)obj;
			if (var.getRootGraph() == requireParentGraph().getRootGraph())
			{
				return Longs.indexOf(_variableGraphTreeIds, var.getGraphTreeId());
			}
		}
		
		return -1;
	}

	@Override
	public Iterator<Variable> iterator()
	{
		return new Wrapper().iterator();
	}

	@NonNullByDefault(false)
	@Override
	public int lastIndexOf(Object obj)
	{
		if (obj instanceof Variable)
		{
			Variable var = (Variable)obj;
			if (var.getRootGraph() == requireParentGraph().getRootGraph())
			{
				return Longs.lastIndexOf(_variableGraphTreeIds, var.getGraphTreeId());
			}
		}
		
		return -1;
	}

	@Override
	public ListIterator<Variable> listIterator()
	{
		return listIterator(0);
	}

	@Override
	public ListIterator<Variable> listIterator(int index)
	{
		return new Wrapper().listIterator(index);
	}

	@Override
	public List<Variable> subList(int fromIndex, int toIndex)
	{
		return new Wrapper().subList(fromIndex, toIndex);
	}

	@Override
	public Object[] toArray()
	{
		return new Wrapper().toArray();
	}

	@SuppressWarnings("unchecked")
	@NonNullByDefault(false)
	@Override
	public <T> T[] toArray(T[] array)
	{
		return new Wrapper().toArray(array);
	}
	
	/*-----------------------
	 * VariableBlock methods
	 */

	/**
	 * Get the {@linkplain Variable#getGraphTreeId() graph tree id} of a variable in the block.
	 * <p>
	 * Because this is the underlying representation, this is faster than calling
	 * <blockquote>
	 * {@code get(index).getGraphTreeId()}.
	 * </blockquote>
	 * <p>
	 * @param index is a non-negative value less than {@link #size}.
	 * @since 0.08
	 * @throws IndexOutOfBoundsException if index is not in valid range.
	 */
	public long getVariableGraphTreeId(int index)
	{
		return _variableGraphTreeIds[index];
	}

	/*--------------------------
	 * Unsupported list methods
	 */
	
	@NonNullByDefault(false)
	@Override
	public boolean add(Variable e)
	{
		throw immutable();
	}

	@NonNullByDefault(false)
	@Override
	public void add(int index, Variable element)
	{
		throw immutable();
	}

	@NonNullByDefault(false)
	@Override
	public boolean addAll(Collection<? extends Variable> c)
	{
		throw immutable();
	}

	@NonNullByDefault(false)
	@Override
	public boolean addAll(int index, Collection<? extends Variable> c)
	{
		throw immutable();
	}

	@Override
	public void clear()
	{
		throw immutable();
	}

	@NonNullByDefault(false)
	@Override
	public boolean remove(Object o)
	{
		throw immutable();
	}

	@Override
	public Variable remove(int index)
	{
		throw immutable();
	}

	@NonNullByDefault(false)
	@Override
	public boolean removeAll(Collection<?> c)
	{
		throw immutable();
	}

	@NonNullByDefault(false)
	@Override
	public boolean retainAll(Collection<?> c)
	{
		throw immutable();
	}


	@NonNullByDefault(false)
	@Override
	public Variable set(int index, Variable element)
	{
		throw immutable();
	}
	
	/*-----------------
	 * Private methods
	 */

	/**
	 * Simple wrapper for this class that allows us to use AbstractList implementations
	 * for some of the non-trivial methods.
	 * 
	 * @since 0.08
	 * @author Christopher Barber
	 */
	private class Wrapper extends AbstractList<Variable>
	{
		@Override
		public Variable get(int index)
		{
			return VariableBlock.this.get(index);
		}

		@Override
		public int size()
		{
			return _variableGraphTreeIds.length;
		}
	}
	
	private static RuntimeException immutable()
	{
		return new UnsupportedOperationException("VariableBlock is immutable");
	}
}
