/*******************************************************************************
*   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.gibbs.samplers.generic;

import static com.analog.lyric.dimple.environment.DimpleEnvironment.*;

import java.util.LinkedList;
import java.util.List;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.dimple.model.domains.Domain;
import com.analog.lyric.dimple.model.values.Value;
import com.analog.lyric.math.DimpleRandom;
import com.analog.lyric.options.DoubleOptionKey;
import com.analog.lyric.options.IOptionHolder;
import com.analog.lyric.options.IntegerOptionKey;
import com.analog.lyric.options.Option;


public class SliceSampler extends AbstractGenericSampler implements IMCMCSampler
{
	private double _initialSliceWidth = 1;	// Default value
	private int _maximumDoublings = 10;	// Default value
	private boolean _explicitInitialSliceWidth = false;
	private boolean _explicitMaximumDoublings = false;
	
	/**
	 * <description>
	 */
	public static final DoubleOptionKey initialSliceWidth =
		new DoubleOptionKey(SliceSampler.class, "initialSliceWidth", 1.0);
	
	/**
	 * <description>
	 */
	public static final IntegerOptionKey maximumDoublings =
		new IntegerOptionKey(SliceSampler.class, "maximumDoublings", 10);
	
	@Override
	public void initialize(Domain variableDomain)
	{
	}
	
	@Override
	public void configureFromOptions(IOptionHolder optionHolder)
	{
		if (!_explicitInitialSliceWidth)
		{
			_initialSliceWidth = optionHolder.getOptionOrDefault(initialSliceWidth);
		}
		if (!_explicitMaximumDoublings)
		{
			_maximumDoublings = optionHolder.getOptionOrDefault(maximumDoublings);
		}
	}
	
	@Override
	public List<Option<?>> getOptionConfiguration(@Nullable List<Option<?>> list)
	{
		if (list == null)
		{
			list = new LinkedList<Option<?>>();
		}
		
		list.add(new Option<Double>(initialSliceWidth, _initialSliceWidth));
		list.add(new Option<Integer>(maximumDoublings, _maximumDoublings));
		
		return list;
	}
	
	@Override
	public boolean nextSample(Value sampleValue, ISamplerClient samplerClient)
	{
		final double y = sampleVerticalSlice(samplerClient);
		final double x = sampleHorizontalSlice(sampleValue.getDouble(), y, (IRealSamplerClient)samplerClient);
		((IRealSamplerClient)samplerClient).setNextSampleValue(x);
		return true;
	}

	// Sample vertical slice in log domain
	// In probability domain this would be uniform from 0 to the value at the current horizontal location (in probability representation)
	public double sampleVerticalSlice(ISamplerClient samplerClient)
	{
		final double yValue = samplerClient.getCurrentSampleScore();
		return yValue - Math.log(activeRandom().nextDouble());
	}

	// Sample horizontal slice using doubling method
	public double sampleHorizontalSlice(double x, double y, IRealSamplerClient samplerClient)
	{
		final DimpleRandom rand = activeRandom();
		
		// First finding slice using doubling method
		double L = x - _initialSliceWidth * rand.nextDouble();
		double R = L + _initialSliceWidth;
		double fL = samplerClient.getSampleScore(L);
		double fR = samplerClient.getSampleScore(R);
		for (int k = 0; k < _maximumDoublings; k++)
		{
			if (y <= fL && y <= fR)
				break;
			if (rand.nextBoolean())	// Flip a coin
			{
				L -= (R - L);
				fL = samplerClient.getSampleScore(L);
			}
			else
			{
				R += (R - L);
				fR = samplerClient.getSampleScore(R);
			}
		}
		
		// Next, shrink the interval as necessary
		double xSample;
		double Ls = L;
		double Rs = R;
		while (true)
		{
			xSample = Ls + (Rs - Ls) * rand.nextDouble();
			double fSample = samplerClient.getSampleScore(xSample);
			
			if (y >= fSample && accept(xSample, x, y, L, R, samplerClient))
				break;	// Accept
			
			// Not accepted yet, shrink the interval
			if (xSample < x)
				Ls = xSample;
			else
				Rs = xSample;
		}
		
		return xSample;
	}
	
	private boolean accept(double xSample, double x, double y, double L, double R, IRealSamplerClient samplerClient)
	{
		boolean D = false;
		double fL = samplerClient.getSampleScore(L);
		double fR = samplerClient.getSampleScore(R);
		
		while (R - L > 1.1 * _initialSliceWidth)
		{
			double M = (L + R) * 0.5;	// Mid-point
			
			if ((x < M && xSample >= M) || (x >= M && xSample < M))
				D = true;
			
			if (xSample < M)
			{
				R = M;
				fR = samplerClient.getSampleScore(R);
			}
			else
			{
				L = M;
				fL = samplerClient.getSampleScore(L);
			}
			
			if (D && (y <= fL) && (y <= fR))
				return false;	// The new point is not acceptable
		}
		
		return true;	// The new point is acceptable
	}

	public double getInitialSliceWidth()
	{
		return _initialSliceWidth;
	}

	/**
	 * @deprecated Will be removed in future release. Instead set {@link #initialSliceWidth} option on
	 * variables or graphs that will be using this sampler.
	 */
	@Deprecated
	public void setInitialSliceWidth(double initialSliceWidth)
	{
		_initialSliceWidth = initialSliceWidth;
		_explicitInitialSliceWidth = true;
	}

	public double getMaximumDoublings()
	{
		return _maximumDoublings;
	}

	/**
	 * @deprecated Will be removed in future release. Instead set {@link #maximumDoublings} option on
	 * variables or graphs that will be using this sampler.
	 */
	@Deprecated
	public void setMaximumDoublings(double maximumDoublings)
	{
		_maximumDoublings = (int)maximumDoublings;
		_explicitMaximumDoublings = true;
	}
}
