/* Copyright (C) 2018 Wildfire Games.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "precompiled.h"
#include "lib/frequency_filter.h"

static const double errorTolerance = 0.25;
static const double g_Sensitivity = 0.10;
static const double sampleTime = 2.0; // seconds

/**
 * variable-width window for frequency determination
 **/
class FrequencyEstimator
{
	NONCOPYABLE(FrequencyEstimator);
public:
	FrequencyEstimator(double resolution)
		: m_minDeltaTime(4.0 * resolution)	// chosen to reduce error but still yield rapid updates.
		, m_lastTime(0)	// will be set on first call
		, m_numEvents(0)
	{
		ENSURE(resolution > 0.0);
	}

	bool operator()(double time, double& frequency)
	{
		m_numEvents++;

		if(m_lastTime == 0.0)
			m_lastTime = time;

		// count # events until deltaTime is large enough
		// (reduces quantization errors if resolution is low)
		const double deltaTime = time - m_lastTime;
		if(deltaTime <= m_minDeltaTime)
			return false;

		frequency = m_numEvents / deltaTime;
		m_numEvents = 0;
		m_lastTime = time;
		return true;	// success
	}

private:
	const double m_minDeltaTime;
	double m_lastTime;
	int m_numEvents;
};


/**
 * variable-gain IIR filter
 **/
class IirFilter
{
public:
	IirFilter(double sensitivity, double initialValue)
		: m_sensitivity(sensitivity), m_prev(initialValue)
	{
	}

	// bias = 0: no change. > 0: increase (n-th root). < 0: decrease (^n)
	double operator()(double x, int bias)
	{
		// sensitivity to changes ([0,1]).
		const double gain = pow(m_sensitivity, ComputeExponent(bias));
		return m_prev = x*gain + m_prev*(1.0-gain);
	}

private:
	static double ComputeExponent(int bias)
	{
		if(bias > 0)
			return 1.0 / bias;	// n-th root
		else if(bias == 0)
			return 1.0;		// no change
		else
			return -bias;	// power-of-n
	}

	double m_sensitivity;
	double m_prev;
};


/**
 * regulate IIR gain for rapid but smooth tracking of a function.
 * this is similar in principle to a PID controller but is tuned for
 * the special case of FPS values to simplify stabilizing the filter.
 **/
class Controller
{
public:
	Controller(double initialValue)
		: m_timesOnSameSide(0)
	{
		std::fill(m_history, m_history+m_historySize, initialValue);
	}

	// bias := exponential change to gain, (-inf, inf)
	int ComputeBias(double smoothedValue, double value)
	{
		if(WasOnSameSide(value))	// (must be checked before updating history)
			m_timesOnSameSide++;
		else
			m_timesOnSameSide = 0;

		// update history
		std::copy(m_history, m_history+m_historySize, m_history+1);
		m_history[m_historySize-1] = value;

		// dampen jitter
		if(Change(smoothedValue, value) < 0.04)
			return -1;

		// dampen spikes/bounces.
		if(WasSpike())
			return -2;

		// if the past few samples have been consistently above/below
		// average, the function is changing and we need to catch up.
		// (similar to I in a PID)
		if(m_timesOnSameSide >= 3)
			return std::min(m_timesOnSameSide, 4);

		// suppress large jumps.
		if(Change(m_history[m_historySize-1], value) > 0.30)
			return -4;	// gain -> 0

		return 0;
	}

private:
	bool WasOnSameSide(double value) const
	{
		int sum = 0;
		for(size_t i = 0; i < m_historySize; i++)
		{
			const int vote = (value >= m_history[i])? 1 : -1;
			sum += vote;
		}
		return abs(sum) == (int)m_historySize;
	}

	static double Change(double from, double to)
	{
		return fabs(from - to) / from;
	}

	// /\ or \/ in last three history entries
	bool WasSpike() const
	{
		cassert(m_historySize >= 3);
		const double h2 = m_history[m_historySize-3], h1 = m_history[m_historySize-2], h0 = m_history[m_historySize-1];
		if(((h2-h1) * (h1-h0)) > 0)	// no sign change
			return false;
		if(Change(h2, h0) > 0.05)	// overall change from oldest to newest value
			return false;
		if(Change(h1, h0) < 0.10)	// no intervening spike
			return false;
		return true;
	}

	static const size_t m_historySize = 3;
	double m_history[m_historySize];
	int m_timesOnSameSide;
};


class FrequencyFilter : public IFrequencyFilter
{
	NONCOPYABLE(FrequencyFilter);
public:
	FrequencyFilter(double resolution, double expectedFrequency)
		: m_frequencyEstimator(resolution), m_controller(expectedFrequency), m_iirFilter(g_Sensitivity, expectedFrequency)
		, m_stableFrequency((int)expectedFrequency), m_smoothedFrequency(expectedFrequency), m_averagedFrequency(expectedFrequency)
		, m_numberOfSamples((int)(sampleTime * expectedFrequency) + 1)
	{
	}

	virtual void Update(double time)
	{
		double frequency;
		if(!m_frequencyEstimator(time, frequency))
			return;

		const int bias = m_controller.ComputeBias(m_smoothedFrequency, frequency);
		m_smoothedFrequency = m_iirFilter(frequency, bias);

		// Keep a moving average of the frequency over the last two seconds
		// If there is a spike of more than 25% (e.g. loading screen => game)
		// then reset the moving average and reset the number of samples needed
		const double difference = fabs(m_smoothedFrequency - m_averagedFrequency);
		if (difference > errorTolerance * m_averagedFrequency)
		{
			m_averagedFrequency = m_smoothedFrequency;
			m_numberOfSamples = (int)(m_averagedFrequency * sampleTime) + 1;
		}
		else
			m_averagedFrequency = ((double)(m_numberOfSamples - 1) * m_averagedFrequency + m_smoothedFrequency) / (double)m_numberOfSamples;

		// allow the smoothed FPS to free-run until it is no longer near the
		// previous stable FPS value. round up because values are more often
		// too low than too high.
		m_stableFrequency = (int)(m_averagedFrequency + 0.99);
	}

	virtual double SmoothedFrequency() const
	{
		return m_smoothedFrequency;
	}

	virtual int StableFrequency() const
	{
		return m_stableFrequency;
	}

private:
	FrequencyEstimator m_frequencyEstimator;
	Controller m_controller;
	IirFilter m_iirFilter;

	int m_stableFrequency;
	double m_smoothedFrequency;
	double m_averagedFrequency;
	int m_numberOfSamples;
};


PIFrequencyFilter CreateFrequencyFilter(double resolution, double expectedFrequency)
{
	return PIFrequencyFilter(new FrequencyFilter(resolution, expectedFrequency));
}
