/*******************************************************************************
*   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.test.model.variables;

import static org.junit.Assert.*;

import java.util.ArrayDeque;
import java.util.Queue;

import org.eclipse.jdt.annotation.Nullable;
import org.junit.Test;

import com.analog.lyric.dimple.data.IDatum;
import com.analog.lyric.dimple.environment.DimpleEnvironment;
import com.analog.lyric.dimple.events.DimpleEventHandler;
import com.analog.lyric.dimple.events.DimpleEventListener;
import com.analog.lyric.dimple.factorfunctions.Normal;
import com.analog.lyric.dimple.model.core.FactorGraph;
import com.analog.lyric.dimple.model.domains.DiscreteDomain;
import com.analog.lyric.dimple.model.values.Value;
import com.analog.lyric.dimple.model.variables.Discrete;
import com.analog.lyric.dimple.model.variables.Real;
import com.analog.lyric.dimple.model.variables.Variable;
import com.analog.lyric.dimple.model.variables.VariableChangeEvent;
import com.analog.lyric.dimple.model.variables.VariableFixedValueChangeEvent;
import com.analog.lyric.dimple.model.variables.VariableInputChangeEvent;
import com.analog.lyric.dimple.model.variables.VariablePriorChangeEvent;
import com.analog.lyric.dimple.solvers.core.parameterizedMessages.DiscreteMessage;
import com.analog.lyric.dimple.solvers.core.parameterizedMessages.DiscreteWeightMessage;
import com.analog.lyric.dimple.test.DimpleTestBase;

/**
 * Test for {@link VariableChangeEvent}s on {@link Variable}.
 * 
 * @since 0.06
 * @author Christopher Barber
 */
@SuppressWarnings("deprecation")
public class TestVariableChangeEvent extends DimpleTestBase
{
	@Test
	public void test()
	{
		//
		// Set up model
		//
		
		FactorGraph fg = new FactorGraph();
		
		Real r = new Real();
		r.setName("r");
		fg.addVariables(r);
		
		Discrete d = new Discrete(DiscreteDomain.bit());
		d.setName("d");
		fg.addVariables(d);
		
		//
		// Set up listener
		//
		
		PriorChangeHandler priorHandler = new PriorChangeHandler();
		FixedValueChangeHandler fixedHandler = new FixedValueChangeHandler();
		InputChangeHandler inputHandler = new InputChangeHandler();
		
		DimpleEventListener listener = DimpleEnvironment.active().createEventListener();
		listener.register(priorHandler, VariablePriorChangeEvent.class, false, fg);
		listener.register(fixedHandler, VariableFixedValueChangeEvent.class, false, fg);
		listener.register(inputHandler, VariableInputChangeEvent.class, false, fg);
		fg.initialize();
		
		assertTrue(listener.isListeningFor(VariablePriorChangeEvent.class, r));
		assertTrue(listener.isListeningFor(VariableFixedValueChangeEvent.class, r));
		
		final Value r2_3 = Value.createReal(2.3);
		final Value r4 = Value.createReal(4.0);
		r.setPrior(r2_3);
		priorHandler.assertEvent(r, VariablePriorChangeEvent.Type.ADDED, null, r2_3);
		fixedHandler.assertEvent(r, VariableFixedValueChangeEvent.Type.ADDED, null, 2.3);
		r.setPrior(r4);
		priorHandler.assertEvent(r, VariablePriorChangeEvent.Type.CHANGED, r2_3, r4);
		fixedHandler.assertEvent(r, VariableFixedValueChangeEvent.Type.CHANGED, 2.3, 4.0);
		r.setPrior(null);
		priorHandler.assertEvent(r, VariablePriorChangeEvent.Type.REMOVED, r4, null);
		fixedHandler.assertEvent(r, VariableFixedValueChangeEvent.Type.REMOVED, 4.0, null);
		priorHandler.assertNoEvent();
		fixedHandler.assertNoEvent();
		inputHandler.assertNoEvent();
		
		DiscreteMessage d37 = new DiscreteWeightMessage(new double[] { .3, .7 });
		DiscreteMessage d46 = new DiscreteWeightMessage(new double[] { .4, .6 });
		d.setPrior(d37);
		priorHandler.assertEvent(d, VariablePriorChangeEvent.Type.ADDED, null, d37);
		fixedHandler.assertNoEvent();
		inputHandler.assertEvent(d, VariableInputChangeEvent.Type.ADDED, null, d37.representation());
		d.setPrior(d46);
		priorHandler.assertEvent(d, VariablePriorChangeEvent.Type.CHANGED, d37, d46);
		inputHandler.assertEvent(d, VariableInputChangeEvent.Type.CHANGED, d37.representation(), d46.representation());
		d.setPrior(null);
		priorHandler.assertEvent(d, VariablePriorChangeEvent.Type.REMOVED, d46, null);
		inputHandler.assertEvent(d, VariableInputChangeEvent.Type.REMOVED, d46.representation(), null);
		
		d.setPrior(d37);
		priorHandler.assertEvent(d, VariablePriorChangeEvent.Type.ADDED, null, d37);
		inputHandler.assertEvent(d, VariableInputChangeEvent.Type.ADDED, null, d37.representation());
		
		final Value d0 = Value.createWithIndex(d.getDomain(), 0);
		d.setPrior(d0);
		priorHandler.assertEvent(d, VariablePriorChangeEvent.Type.CHANGED, d37, d0);
		fixedHandler.assertEvent(d, VariableFixedValueChangeEvent.Type.ADDED, null, 0);
		inputHandler.assertEvent(d, VariableInputChangeEvent.Type.REMOVED, d37.representation(), null);
		d.setPrior(d37);
		priorHandler.assertEvent(d, VariablePriorChangeEvent.Type.CHANGED, d0, d37);
		inputHandler.assertEvent(d, VariableInputChangeEvent.Type.ADDED, null, d37.representation());
		fixedHandler.assertEvent(d, VariableFixedValueChangeEvent.Type.REMOVED, 0, null);
		
		Normal normal = new Normal(1.0, 1.0);
		r.setPrior(normal);
		priorHandler.assertEvent(r, VariablePriorChangeEvent.Type.ADDED, null, normal);
		inputHandler.assertEvent(r, VariableInputChangeEvent.Type.ADDED, null, normal);
		fixedHandler.assertNoEvent();
		final Value r3 = Value.createReal(3.0);
		r.setPrior(r3);
		priorHandler.assertEvent(r, VariablePriorChangeEvent.Type.CHANGED, normal, r3);
		inputHandler.assertEvent(r, VariableInputChangeEvent.Type.REMOVED, normal, null);
		fixedHandler.assertEvent(r, VariableFixedValueChangeEvent.Type.ADDED, null, 3.0);
		r.setInput(normal);
		priorHandler.assertEvent(r, VariablePriorChangeEvent.Type.CHANGED, r3, normal);
		inputHandler.assertEvent(r, VariableInputChangeEvent.Type.ADDED, null, normal);
		fixedHandler.assertEvent(r, VariableFixedValueChangeEvent.Type.REMOVED, 3.0, null);
		
		priorHandler.assertNoEvent();
		inputHandler.assertNoEvent();
		fixedHandler.assertNoEvent();
		
		listener.block(VariablePriorChangeEvent.class, false, r);
		listener.block(VariableFixedValueChangeEvent.class, false, r);
		r.notifyListenerChanged();
		assertFalse(listener.isListeningFor(VariablePriorChangeEvent.class, r));
		assertFalse(listener.isListeningFor(VariableFixedValueChangeEvent.class, r));

		final Value r1 = Value.createReal(1.0);
		r.setPrior(r1);
		priorHandler.assertNoEvent();
		fixedHandler.assertNoEvent();
		inputHandler.assertEvent(r, VariableInputChangeEvent.Type.REMOVED, normal, null);

		listener.block(VariableInputChangeEvent.class,  false, r);
		r.notifyListenerChanged();
		d.notifyListenerChanged();

		assertTrue(listener.isListeningFor(VariablePriorChangeEvent.class, d));
		assertTrue(listener.isListeningFor(VariableFixedValueChangeEvent.class, d));
		assertTrue(listener.isListeningFor(VariableInputChangeEvent.class, d));
		
		final Value d1 = Value.create(d.getDomain(), 1.0);
		d.setPrior(d1);
		priorHandler.assertEvent(d, VariablePriorChangeEvent.Type.CHANGED, d37, d1);
		fixedHandler.assertEvent(d, VariableFixedValueChangeEvent.Type.ADDED, null, 1);
		inputHandler.assertEvent(d, VariableInputChangeEvent.Type.REMOVED, d37.representation(), null);
			
		listener.unblock(VariablePriorChangeEvent.class, r);
		listener.unblock(VariableFixedValueChangeEvent.class, r);
		r.notifyListenerChanged();
		
		r.setPrior(normal);
		priorHandler.assertEvent(r, VariablePriorChangeEvent.Type.CHANGED, r1, normal);
		inputHandler.assertNoEvent();
		fixedHandler.assertEvent(r, VariableFixedValueChangeEvent.Type.REMOVED, 1.0, null);
		
		DimpleEnvironment.active().setEventListener(null);
		
		r.setPrior(r2_3);
		priorHandler.assertNoEvent();
		inputHandler.assertNoEvent();
		fixedHandler.assertNoEvent();
	}
	
	private static class PriorChangeHandler extends DimpleEventHandler<VariablePriorChangeEvent>
	{
		private Queue<VariablePriorChangeEvent> _events = new ArrayDeque<VariablePriorChangeEvent>();
		
		@Override
		public void handleEvent(VariablePriorChangeEvent event)
		{
			Variable var = event.getModelObject();
			assertEquals(var.getPrior(), event.getNewPrior());
			switch (event.getType())
			{
			case ADDED:
				assertNotNull(var.getPrior());
				assertNull(event.getOldPrior());
				break;
			case REMOVED:
				assertNull(var.getPrior());
				assertNotNull(event.getOldPrior());
				break;
			case CHANGED:
				assertNotNull(event.getNewPrior());
				assertNotNull(event.getOldPrior());
				assertNotEquals(event.getNewPrior(), event.getOldPrior());
			}
			_events.add(event);
		}
		
		void assertEvent(Variable var, VariablePriorChangeEvent.Type type,
			@Nullable IDatum oldValue, @Nullable IDatum newValue)
		{
			VariablePriorChangeEvent event = _events.poll();
			assertNotNull(event);
			assertSame(var, event.getModelObject());
			assertEquals(type, event.getType());
			assertEquals(oldValue, event.getOldPrior());
			assertEquals(newValue, event.getNewPrior());
		}
		
		void assertNoEvent()
		{
			assertTrue(_events.isEmpty());
		}
	}

	private static class FixedValueChangeHandler extends DimpleEventHandler<VariableFixedValueChangeEvent>
	{
		private Queue<VariableFixedValueChangeEvent> _events = new ArrayDeque<VariableFixedValueChangeEvent>();
		
		@Override
		public void handleEvent(VariableFixedValueChangeEvent event)
		{
			Variable var = event.getModelObject();
			assertEquals(var.getFixedValueObject(), event.getNewValue());
			switch (event.getType())
			{
			case ADDED:
				assertTrue(var.hasFixedValue());
				assertNull(event.getOldValue());
				break;
			case REMOVED:
				assertFalse(var.hasFixedValue());
				assertNull(event.getNewValue());
				assertNotNull(event.getOldValue());
				break;
			case CHANGED:
				assertTrue(var.hasFixedValue());
				assertNotNull(event.getNewValue());
				assertNotNull(event.getOldValue());
			}
			_events.add(event);
		}
		
		void assertEvent(Variable var, VariableFixedValueChangeEvent.Type type,
			@Nullable Object oldValue, @Nullable Object newValue)
		{
			VariableFixedValueChangeEvent event = _events.poll();
			assertNotNull(event);
			assertSame(var, event.getModelObject());
			assertEquals(type, event.getType());
			assertEquals(oldValue, event.getOldValue());
			assertEquals(newValue, event.getNewValue());
		}
		
		void assertNoEvent()
		{
			assertTrue(_events.isEmpty());
		}
	}
	private static class InputChangeHandler extends DimpleEventHandler<VariableInputChangeEvent>
	{
		private Queue<VariableInputChangeEvent> _events = new ArrayDeque<VariableInputChangeEvent>();
		
		@Override
		public void handleEvent(VariableInputChangeEvent event)
		{
			Variable var = event.getModelObject();
			if (event.getNewInput() != null)
			{
				assertInputsEqual(var.getInputObject(), event.getNewInput());
			}
			switch (event.getType())
			{
			case ADDED:
				assertTrue(!var.hasFixedValue());
				assertNull(event.getOldInput());
				break;
			case REMOVED:
				assertNull(event.getNewInput());
				assertNotNull(event.getOldInput());
				break;
			case CHANGED:
				assertNotNull(event.getNewInput());
				assertNotNull(event.getOldInput());
			}
			_events.add(event);
		}
		
		void assertEvent(Variable var, VariableInputChangeEvent.Type type,
			@Nullable Object oldInput, @Nullable Object newInput)
		{
			VariableInputChangeEvent event = _events.poll();
			assertNotNull(event);
			assertSame(var, event.getModelObject());
			assertEquals(type, event.getType());
			assertInputsEqual(oldInput, event.getOldInput());
			assertInputsEqual(newInput, event.getNewInput());
		}

		void assertNoEvent()
		{
			assertTrue(_events.isEmpty());
		}
	}
	
	static void assertInputsEqual(@Nullable Object input1, @Nullable Object input2)
	{
		if (input1 instanceof double[])
		{
			assertArrayEquals((double[])input1, (double[])input2, 1e-15);
		}
		else
		{
			assertEquals(input1, input2);
		}
	}
	
}
