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

import static org.junit.Assert.*;

import java.util.concurrent.TimeUnit;

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

import com.analog.lyric.dimple.exceptions.DimpleException;
import com.analog.lyric.dimple.model.domains.RealDomain;
import com.analog.lyric.dimple.parameters.IParameterKey;
import com.analog.lyric.dimple.parameters.IParameterList;
import com.analog.lyric.dimple.parameters.Parameter;
import com.analog.lyric.dimple.parameters.ParameterKey;
import com.analog.lyric.dimple.parameters.ParameterList1;
import com.analog.lyric.dimple.parameters.ParameterList2;
import com.analog.lyric.dimple.parameters.ParameterListN;
import com.analog.lyric.dimple.parameters.SharedParameterValue;
import com.analog.lyric.dimple.test.DimpleTestBase;
import com.analog.lyric.options.IOptionHolder;
import com.analog.lyric.options.IOptionKey;
import com.analog.lyric.util.test.SerializationTester;
import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.AtomicDouble;
import com.google.common.util.concurrent.AtomicDoubleArray;

public class TestParameters extends DimpleTestBase
{
	/*---------------
	 * Test classes
	 */
	
	public static class ParamN extends ParameterListN<IParameterKey>
	{
		private static final long serialVersionUID = 1L;

		public ParamN(int size)
		{
			super(size);
		}
		
		public ParamN(ParamN that)
		{
			super(that);
		}
		
		public ParamN(double ... values)
		{
			super(values);
		}
		
		public ParamN(boolean fixed, double ... values)
		{
			super(fixed, values);
		}

		@Override
		public @Nullable IParameterKey[] getKeys()
		{
			return null;
		}

		@Override
		public ParamN clone()
		{
			return new ParamN(this);
		}
	}
	
	@SuppressWarnings("null")
	public static enum Alpha implements IParameterKey
	{
		alpha(0, RealDomain.unbounded());
		
		private final double _defaultValue;
		private final RealDomain _domain;

		private Alpha(double defaultValue, RealDomain domain)
		{
			_defaultValue = defaultValue;
			_domain = domain;
		}

		@Override
		public Class<Double> type() { return Double.class; }
		@Override
		public Object convertToExternal(Double value)
		{
			return value;
		}
		@Override
		public Double convertToValue(Object value) { return type().cast(value); }
		@Override
		public Double defaultValue() { return _defaultValue; }
		@Override
		public Double getOrDefault(IOptionHolder holder) { return holder.getOptionOrDefault(this); }
		@Override
		public @Nullable Double get(IOptionHolder holder) { return holder.getOption(this); }
		@Override
		public boolean local() { return false; }
		@Override
		public IOptionKey.Lookup lookupMethod()
		{
			return IOptionKey.Lookup.NONLOCAL;
		}
		@Override
		public void set(IOptionHolder holder, Double value) { holder.setOption(this,  value); }

		@Override
		public void unset(IOptionHolder holder) { holder.unsetOption(this); }
		@Override
		public RealDomain domain() { return _domain; }
		@Override
		public boolean validForDelegator(Double value, IOptionHolder delegator)
		{
			return true;
		}
		@Override
		public Double validate(Double value, IOptionHolder optionHolder)
		{
			return type().cast(value);
		}
	}
	
	public static class Param1 extends ParameterList1<Alpha>
	{
		private static final long serialVersionUID = 1L;

		public Param1()
		{
		}
		
		public Param1(Param1 that)
		{
			super(that);
		}
		
		@Override
		public Alpha[] getKeys()
		{
			return Alpha.values();
		}

		@Override
		public Param1 clone()
		{
			return new Param1(this);
		}
	}
	
	public static class AlphaBeta
	{
		public static final ParameterKey alpha = new ParameterKey(0, AlphaBeta.class, "alpha");
		public static final ParameterKey beta = new ParameterKey(1, AlphaBeta.class, "beta");
		
		public static ParameterKey[] values()
		{
			return new ParameterKey[] { alpha, beta };
		}
	}
	
	public static class Param2 extends ParameterList2<ParameterKey>
	{
		private static final long serialVersionUID = 1L;

		public Param2()
		{
		}
		
		public Param2(Param2 that)
		{
			super(that);
		}
		
		@Override
		public ParameterKey[] getKeys()
		{
			return AlphaBeta.values();
		}

		@Override
		public ParameterList2<ParameterKey> clone()
		{
			return new Param2(this);
		}
	}
	
	/*------------
	 * Test cases
	 */
	
	@Test
	public void testParameterList()
	{
		Param1 n1 = new Param1();
		assertEquals(1, n1.size());
		assertParameterListInvariants(n1);
		assertFalse(n1.isShared(Alpha.alpha));
		
		Param2 n2 = new Param2();
		assertEquals(2, n2.size());
		assertParameterListInvariants(n2);
		assertFalse(n2.isShared(0));
		assertFalse(n2.isShared(AlphaBeta.beta));
		
		// Parameter sharing
		assertTrue(n1.canShare());
		assertTrue(n2.canShare());
		n2.setShared(AlphaBeta.alpha, true);
		assertNotNull(n2.getSharedValue(AlphaBeta.alpha));
		n1.setSharedValue(Alpha.alpha, n2.getSharedValue(AlphaBeta.alpha));
		assertSame(n1.getSharedValue(Alpha.alpha), n2.getSharedValue(AlphaBeta.alpha));
		n1.set(0, 42);
		assertEquals(42, n2.get(0), 0.0);
		
		n2.setFixed(AlphaBeta.beta, true);
		assertParameterListInvariants(n1);
		assertParameterListInvariants(n2);
		
		ParamN n10 = new ParamN(10);
		assertEquals(10, n10.size());
		assertParameterListInvariants(n10);
		assertFalse(n10.canShare());
	
		n10 = new ParamN(1,2,3,4,5,6,7,8,9,10);
		assertEquals(10, n10.size());
		assertParameterListInvariants(n10);
		for (int i = 0; i < n10.size(); ++i)
		{
			assertEquals(i + 1, n10.get(i), 0.0);
		}
		
		ParamN n5 = new ParamN(true, 0, 2, 4, 6, 8);
		assertEquals(5, n5.size());
		assertParameterListInvariants(n5);
		for (int i = 0; i < 5; ++i)
		{
			assertTrue(n5.isFixed(i));
			assertEquals(i * 2, n5.get(i), 0.0);
		}
	}
	
	private double d;
	private double d2;
	
	@Test
	@Ignore
	public void testAtomicDoublePerformance()
	{
		// Measure performance of AtomicDouble types used by parameter class implementations.
		// Slowdown appears to be about 4.5x for AtomicDoubleArray and 2x for AtomicDouble
		
		boolean verbose = true;
		
		AtomicDoubleArray n100 = new AtomicDoubleArray(100);
		double[] d100 = new double[100];
		double[] out100 = new double[100];
		
		Stopwatch stopwatch = Stopwatch.createUnstarted();
		
		final int n = 1000;
		
		stopwatch.start();
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < 100; ++j)
			{
				n100.set(j, j);
			}
		}
		stopwatch.stop();
		
		long atomicNs = stopwatch.elapsed(TimeUnit.NANOSECONDS);
		
		stopwatch.reset();
		stopwatch.start();
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < 100; ++j)
			{
				d100[j] = j;
			}
		}
		stopwatch.stop();
		
		long doubleNs = stopwatch.elapsed(TimeUnit.NANOSECONDS);
		
		double atomicNsPerWrite = atomicNs / (100 * n);
		double doubleNsPerWrite = doubleNs / (100 * n);
		double ratio = atomicNsPerWrite / doubleNsPerWrite;
			
		if (verbose)
		{
			System.out.format("AtomicDoubleArray/double[] WRITE: %.0f/%.0f ns/write (%.1fX slowdown)\n",
				atomicNsPerWrite, doubleNsPerWrite, ratio);
		}

		stopwatch.start();
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < 100; ++j)
			{
				out100[j] = n100.get(j);
			}
		}
		stopwatch.stop();
		
		atomicNs = stopwatch.elapsed(TimeUnit.NANOSECONDS);
		
		stopwatch.reset();
		stopwatch.start();
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < 100; ++j)
			{
				out100[j] = d100[j];
			}
		}
		stopwatch.stop();
		
		doubleNs = stopwatch.elapsed(TimeUnit.NANOSECONDS);
		
		atomicNsPerWrite = atomicNs / (100 * n);
		doubleNsPerWrite = doubleNs / (100 * n);
		ratio = atomicNsPerWrite / doubleNsPerWrite;
			
		if (verbose)
		{
			System.out.format("AtomicDoubleArray/double[] READ: %.0f/%.0f ns/read (%.1fX slowdown)\n",
				atomicNsPerWrite, doubleNsPerWrite, ratio);
		}
		
		AtomicDouble a = new AtomicDouble();
//		double d = 0.0, d2 = 0.0;
		
		final int n2 = n * 100;
		
		stopwatch.reset();
		stopwatch.start();
		for (int i = 0; i < n2; ++i)
		{
			a.set(d2);
		}
		stopwatch.stop();
		atomicNs = stopwatch.elapsed(TimeUnit.NANOSECONDS);

		stopwatch.reset();
		stopwatch.start();
		for (int i = 0; i < n2; ++i)
		{
			d = d2;
		}
		stopwatch.stop();
		doubleNs = stopwatch.elapsed(TimeUnit.NANOSECONDS);
		
		atomicNsPerWrite = atomicNs / n2;
		doubleNsPerWrite = doubleNs / n2;
		ratio = atomicNsPerWrite / doubleNsPerWrite;
			
		if (verbose)
		{
			System.out.format("AtomicDouble/double WRITE: %.0f/%.0f ns/write (%.1fX slowdown)\n",
				atomicNsPerWrite, doubleNsPerWrite, ratio);
		}

		stopwatch.reset();
		stopwatch.start();
		for (int i = 0; i < n2; ++i)
		{
			d2 = a.get();
		}
		stopwatch.stop();
		atomicNs = stopwatch.elapsed(TimeUnit.NANOSECONDS);

		stopwatch.reset();
		stopwatch.start();
		for (int i = 0; i < n2; ++i)
		{
			d2 = d;
		}
		stopwatch.stop();
		doubleNs = stopwatch.elapsed(TimeUnit.NANOSECONDS);
		
		atomicNsPerWrite = atomicNs / n2;
		doubleNsPerWrite = doubleNs / n2;
		ratio = atomicNsPerWrite / doubleNsPerWrite;
			
		if (verbose)
		{
			System.out.format("AtomicDouble/double READ: %.0f/%.0f ns/read (%.1fX slowdown)\n",
				atomicNsPerWrite, doubleNsPerWrite, ratio);
		}
	}
	
	/*-----------------
	 * Helper methods
	 */

	public static <Key extends IParameterKey> void assertParameterListInvariants(IParameterList<Key> list)
	{
		assertTrue(list.size() > 0);
		double[] values = list.getValues();
		assertEquals(list.size(), values.length);
		
		for (int i = 0; i < values.length; ++i)
		{
			double val = list.get(i);
			assertEquals(val, values[i], 0.0);
			
			SharedParameterValue sharedValue = list.getSharedValue(i);
			assertEquals(list.isShared(i), sharedValue != null);
			if (sharedValue != null)
			{
				assertEquals(val, sharedValue.get(), 0.0);
			}
			
			if (list.isFixed(i))
			{
				try
				{
					list.set(i, val + 1);
					fail("should not get here");
				}
				catch (DimpleException ex)
				{
				}
				assertEquals(val, list.get(i), 0.0);
			}
		}
		
		Key[] keys = list.getKeys();
		
		assertEquals(list.hasKeys(), keys != null);
		if (keys != null)
		{
			assertEquals(list.size(), keys.length);
			for (int i = 0; i < keys.length; ++i)
			{
				Key key = keys[i];
				double val = list.get(i);
				assertEquals(i, key.ordinal());
				assertEquals(val, list.get(key), 0.0);
				assertEquals(list.getSharedValue(i), list.getSharedValue(key));
				assertEquals(list.isFixed(i),  list.isFixed(key));
				assertEquals(list.isShared(i), list.isShared(key));
				
				if (list.isFixed(key))
				{
					try
					{
						list.set(key, val + 1);
						fail("should not get here");
					}
					catch (DimpleException ex)
					{
					}
					assertEquals(val, list.get(key), 0.0);
				}
			}
		}
		
		{
			int i = 0;
			for (Parameter<Key> param : list)
			{
				assertEquals(i, param.index());
				assertEquals(list.get(i), param.value(), 0.0);
				assertSame(list.getSharedValue(i), param.sharedValue());
				if (keys != null)
				{
					assertEquals(keys[i], param.key());
				}
				assertEquals(!Double.isNaN(list.get(i)), param.known());
				++i;
			}
		}
		
		//
		// Errors
		//
		
		try
		{
			list.get(-1);
			fail("should not get here");
		}
		catch (IndexOutOfBoundsException ex)
		{
		}
		
		try
		{
			list.get(list.size());
			fail("should not get here");
		}
		catch (IndexOutOfBoundsException ex)
		{
		}
		
		if (keys == null)
		{
			try
			{
				@SuppressWarnings("unchecked")
				Key key = (Key) new ParameterKey(0, Object.class, "bogus");
				list.get(key);
				fail("should not get here");
			}
			catch (UnsupportedOperationException ex)
			{
			}
		}
		
		if (!list.canShare())
		{
			try
			{
				list.setShared(0, true);
				fail("should not get here");
			}
			catch (UnsupportedOperationException ex)
			{
			}
			assertFalse(list.isShared(0));
			
			list.setShared(0, false);
			
			try
			{
				list.setSharedValue(0, new SharedParameterValue());
				fail("should not get here");
			}
			catch (UnsupportedOperationException ex)
			{
			}
			
			list.setSharedValue(0, null);
		}
		
		//
		// Cloning
		//
		
		IParameterList<Key> copy1 = list.clone();
		assertNotSame(copy1, list);
		assertParameterListEquals(list, copy1);
		for (int i = 0; i < list.size(); ++i)
		{
			assertSame(list.getSharedValue(i), copy1.getSharedValue(i));
		}
		
		IParameterList<Key> copy2 = SerializationTester.clone(list);
		assertNotSame(copy2, list);
		assertParameterListEquals(list, copy2);
		
		//
		// Test mutable cases on cloned list
		//
		
		copy2.setAllFixed(false);
		for (int i = 0; i < copy2.size(); ++i)
		{
			copy2.setShared(i, false);
			assertFalse(copy2.isShared(i));
		}
		for (int i = 0; i < copy2.size(); ++i)
		{
			assertFalse(copy2.isFixed(i));
			copy2.set(i, i);
			assertEquals(i, copy2.get(i), 0.0);
		}
		
		copy2.setAllMissing();
		for (Parameter<Key> param : copy2)
		{
			assertFalse(param.known());
		}
		
		copy2.setAllToDefault();
		if (keys != null)
		{
			for (int i = 0; i <copy2.size(); ++i)
			{
				assertEquals(keys[i].defaultValue(), copy2.get(i), 0.0);
			}
		}
		
		copy2.setAll(copy1.getValues());
		
		copy2.setAllFixed(true);
		copy2.setAllToDefault(); // Does nothing if everything is fixed
		copy2.setAllMissing(); // ditto
		try
		{
			copy2.setAll(new double[list.size()]);
			fail("should not get here");
		}
		catch (DimpleException ex)
		{
		}
		for (int i = 0; i < copy2.size(); ++i)
		{
			assertTrue(copy2.isFixed(i));
			assertEquals(list.get(i), copy2.get(i), 0.0);
		}
		
		// Fix every-other parameter, and set non-fixed params.
		for (int i = 0; i < copy2.size(); i += 2)
		{
			copy2.setFixed(i, false);
		}
		copy2.setAllMissing();
		for (Parameter<Key> param : copy2)
		{
			switch (param.index() % 2)
			{
			case 0:
				assertFalse(param.fixed());
				assertFalse(param.known());
				break;
			case 1:
				assertTrue(param.fixed());
				assertEquals(copy2.get(param.index()), param.value(), 0.0);
				break;
			}
		}
		
		for (int i = 0; i < copy2.size(); ++i)
		{
			copy2.setFixed(i, !copy2.isFixed(i));
		}
		copy2.setAllToDefault();
		for (Parameter<Key> param : copy2)
		{
			switch (param.index() % 2)
			{
			case 0:
				assertTrue(param.fixed());
				assertFalse(param.known());
				break;
			case 1:
				assertFalse(param.fixed());
				Key key = param.key();
				if (key != null)
				{
					assertEquals(key.defaultValue(), param.value(), 0.0);
				}
				else
				{
					assertEquals(copy2.get(param.index()), param.value(), 0.0);
				}
				break;
			}
		}
		
	} // assertParameterListInvariants
	
	public static <Key extends IParameterKey> void assertParameterListEquals(IParameterList<Key> list1,
		IParameterList<Key> list2)
	{
		assertEquals(list1.getClass(), list2.getClass());
		assertEquals(list1.size(), list2.size());
		assertArrayEquals(list1.getValues(), list2.getValues(), 0.0);
		assertArrayEquals(list1.getKeys(), list2.getKeys());
		for (int i = list1.size(); --i >=0;)
		{
			assertEquals(list1.isFixed(i), list2.isFixed(i));
			assertEquals(list1.isShared(i), list2.isShared(i));
		}
	}
}
