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

import java.util.Objects;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.dimple.events.IDimpleEventListener;
import com.analog.lyric.dimple.events.SolverEvent;
import com.analog.lyric.dimple.exceptions.DimpleException;
import com.analog.lyric.dimple.model.core.Node;
import com.analog.lyric.dimple.solvers.core.parameterizedMessages.IParameterizedMessage;
import com.analog.lyric.dimple.solvers.interfaces.ISolverEdgeState;
import com.analog.lyric.dimple.solvers.interfaces.ISolverFactorGraph;
import com.analog.lyric.dimple.solvers.interfaces.ISolverNode;

/**
 * Abstract base implementation of {@link ISolverNode}
 * @param <MNode> is the corresponding model {@link Node} type.
 */
public abstract class SNode<MNode extends Node> extends SChild<MNode> implements ISolverNode
{
	/*-----------
	 * Constants
	 */
	
	/**
	 * Bits in {@link #_flags} reserved by this class and its superclasses.
	 */
	@SuppressWarnings("hiding")
	protected static final int RESERVED_FLAGS = 0xFF000000;

	private static final int MESSAGE_EVENT_MASK = 0x03000000;

	private static final int MESSAGE_EVENT_UNKNOWN = 0x00000000;

	private static final int MESSAGE_EVENT_NONE = 0x01000000;

	private static final int MESSAGE_EVENT_ENABLED = 0x03000000;
	
	protected static final int EVENT_MASK = MESSAGE_EVENT_MASK;
	
	/*--------------
	 * Construction
	 */
	
	public SNode(MNode n)
	{
		super(n);
	}
	
	@Override
	public String toString()
	{
		return String.format("[%s %s]", getClass().getSimpleName(), _model.getQualifiedName());
	}
	
	/*---------------------
	 * ISolverNode methods
	 */
	
	@Override
	public ISolverNode getSibling(int edge)
	{
		final Node sibling = getModelObject().getSibling(edge);
		return getSolverMapping().getSolverNode(sibling);
	}
	
	@Override
	public int getSiblingCount()
	{
		return _model.getSiblingCount();
	}
	
	/**
	 * Remove solver edge state for specified sibling of this node.
	 * 
	 * @param edge is a non-negative index less than {@link #getSiblingCount()}.
	 * @since 0.08
	 */
	public void removeSiblingEdgeState(int edge)
	{
		Node node = _model;
		if (edge < node.getSiblingCount())
		{
			requireParentGraph().removeSolverEdge(node.getSiblingEdgeState(edge));
		}
	}
	
	/**
	 * Remove solver edge state for all siblings of this node.
	 * 
	 * @since 0.08
	 */
	public void removeSiblingEdgeState()
	{
		final Node node = _model;
		final ISolverFactorGraph parent = requireParentGraph();
		
		for (int edge = 0, n = node.getSiblingCount(); edge < n; ++edge)
		{
			parent.removeSolverEdge(node.getSiblingEdgeState(edge));
		}
	}

	@Deprecated
	@Override
	public void setInputMsg(int portIndex, Object obj)
	{
		setInputMsgValues(portIndex, obj);
	}
	
	@Deprecated
	@Override
	public void setOutputMsg(int portIndex, Object obj)
	{
		setOutputMsgValues(portIndex, obj);
	}

	@Deprecated
	@Override
	public void setInputMsgValues(int portIndex, Object obj)
	{
		throw new DimpleException("Not supported by " + this);
	}
	
	@Deprecated
	@Override
	public void setOutputMsgValues(int portIndex, Object obj)
	{
		throw new DimpleException("Not supported by " + this);
	}
	
	@Override
	public void update()
	{
		if (raiseMessageEvents())
		{
			final IParameterizedMessage[] oldMessages = cloneMessages();
			
			doUpdate();
			
			final IParameterizedMessage[] newMessages = cloneMessages();
			if (newMessages != null)
			{
				for (int edge = 0, nEdges = newMessages.length; edge < nEdges; ++edge)
				{
					final IParameterizedMessage oldMessage = oldMessages != null ? oldMessages[edge] : null;
					final IParameterizedMessage newMessage = newMessages[edge];

					if (newMessage != null)
					{
						raiseEvent(createMessageEvent(edge, oldMessage, newMessage));
					}
				}
			}
		}
		else
		{
			doUpdate();
		}
	}

	@Override
	public void updateEdge(int edge)
	{
		if (raiseMessageEvents())
		{
			final IParameterizedMessage oldMessage = cloneMessage(edge);
			
			doUpdateEdge(edge);
			
			final IParameterizedMessage newMessage = cloneMessage(edge);
			if (newMessage != null)
			{
				raiseEvent(createMessageEvent(edge, oldMessage, newMessage));
			}
		}
		else
		{
			doUpdateEdge(edge);
		}
	}

	/*---------------------------
	 * SolverEventSource methods
	 */
	
    @Override
    protected int getEventMask()
    {
    	return EVENT_MASK;
    }

	/*-------------------------
	 * Protected SNode methods
	 */
	
	/**
	 * Returns a clone of outgoing message for given {@code edge}.
	 * <p>
	 * @param edge index in the range [0, {@link #getSiblingCount()}-1].
	 * @return clone of message if applicable. May return null if there is no message or if subclass
	 * does not support messages. In the latter case, {@link #supportsMessageEvents()} should return false.
	 * <p>
	 * The default implementation returns null.
	 * <p>
	 * Subclasses that override this method to return a non-null message should also override
	 * {@link #supportsMessageEvents()}.
	 * <p>
	 * @since 0.06
	 */
	protected @Nullable IParameterizedMessage cloneMessage(int edge)
	{
		return null;
	}

	private final @Nullable IParameterizedMessage[] cloneMessages()
	{
		IParameterizedMessage[] messages = null;
		
		final int size = getSiblingCount();
		if (size > 0)
		{
			final IParameterizedMessage firstMessage = cloneMessage(0);
			if (firstMessage != null)
			{
				messages = new IParameterizedMessage[size];
				messages[0] = firstMessage;
				for (int i = 1; i < size; ++i)
				{
					messages[i] = cloneMessage(i);
				}
			}
		}
		
		return messages;
	}

	protected void doUpdate()
	{
		for (int i = 0, end = getSiblingCount(); i < end; i++)
		{
			doUpdateEdge(i);
		}
	}

	protected abstract void doUpdateEdge(int edge);

	/**
	 * Creates a {@link IMessageUpdateEvent} event.
	 * <p>
	 * Default implementation returns null. When this method returns null, then {@link #supportsMessageEvents()}
	 * should also return false.
	 * <p>
	 * @param edge is the outgoing edge for the messages.
	 * @param oldMessage is the previous value of the message, which may be null.
	 * @param newMessage is the new message value, which must not be null.
	 * @since 0.06
	 */
	protected @Nullable SolverEvent createMessageEvent(
		int edge,
		@Nullable IParameterizedMessage oldMessage,
		IParameterizedMessage newMessage)
	{
		return null;
	}
	
	@Override
	public @Nullable ISolverEdgeState getSiblingEdgeState(int siblingIndex)
	{
		return requireParentGraph().getSolverEdge(_model.getSiblingEdgeIndex(siblingIndex));
	}
	
	/**
	 * If {@link #supportsMessageEvents()}, this returns the base type for all message
	 * events that can be created by this object. Default implementation returns null.
	 * @since 0.06
	 */
	protected @Nullable Class<? extends SolverEvent> messageEventType()
	{
		return null;
	}

	/**
	 * Indicate subclass has a concept of passing a {@link IParameterizedMessage} messages.
	 * <p>
	 * Should only return true if:
	 * <ul>
	 * <li>{@link #cloneMessage(int)} can return non-null message
	 * <li>{@link #messageEventType()} is non-null
	 * <li>{@link #createMessageEvent} can return a non-null event
	 * </ul>
	 * Since {@link SVariableBase} and {@link SFactorBase} implement the latter two methods, subclasses
	 * of those classes that support message events only need to implement {@link #cloneMessage(int)} and
	 * this method.
	 * <p>
	 * The default implementation returns false.
	 * <p>
	 * @since 0.06
	 */
	protected boolean supportsMessageEvents()
	{
		return false;
	}
	
	/*-----------------
	 * Private methods
	 */

	/**
	 * Indicates whether to generate {@link IMessageUpdateEvent}s.
	 * @since 0.06
	 */
	private boolean raiseMessageEvents()
	{
		final int flags = _flags & MESSAGE_EVENT_MASK;
		
		if (flags == MESSAGE_EVENT_NONE)
		{
			// Check this first to minimize overhead when known to be disabled
			return false;
		}
		else if (flags == MESSAGE_EVENT_UNKNOWN)
		{
			boolean enabled = false;
			if (supportsMessageEvents())
			{
				final IDimpleEventListener listener = getEventListener();
				if (listener != null)
				{
					enabled = listener.isListeningFor(Objects.requireNonNull(messageEventType()), this);
				}
			}
			setFlagValue(MESSAGE_EVENT_MASK, enabled ? MESSAGE_EVENT_ENABLED : MESSAGE_EVENT_NONE);
			return enabled;
		}
		else
		{
			return flags == MESSAGE_EVENT_ENABLED;
		}
	}
}
