/*
 * Copyright 2009-2019 the original author or authors.
 *
 * 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
 *
 *      https://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 org.springframework.integration.support.management;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;



/**
 * Cumulative statistics for a series of real numbers with higher weight given to recent data.
 * Clients call {@link #append(double)} every time there is a new measurement, and then can collect summary
 * statistics from the convenience getters (e.g. {@link #getStatistics()}). Older values are given exponentially smaller
 * weight, with a decay factor determined by a "window" size chosen by the caller. The result is a good approximation to
 * the statistics of the series but with more weight given to recent measurements, so if the statistics change over time
 * those trends can be approximately reflected. For performance reasons, the calculation is performed on retrieval,
 * {@code window * 5} samples are retained meaning that the earliest retained value contributes just 0.5% to the
 * sum.
 * @deprecated in favor of dimensional metrics via
 * {@link org.springframework.integration.support.management.metrics.MeterFacade}.
 * Built-in metrics will be removed in a future release.
 *
 * @author Dave Syer
 * @author Gary Russell
 * @since 2.0
 */
@Deprecated
public class ExponentialMovingAverage {

	private volatile long count;

	private volatile double min = Double.MAX_VALUE;

	private volatile double max;

	private final Deque<Double> samples = new ArrayDeque<>();

	private final int retention;

	private final int window;

	private final double factor;


	/**
	 * Create a moving average accumulator with decay lapse window provided. Measurements older than this will have
	 * smaller weight than <code>1/e</code>.
	 * @param window the exponential lapse window (number of measurements)
	 */
	public ExponentialMovingAverage(int window) {
		this(window, 1);
	}

	/**
	 * Create a moving average accumulator with decay lapse window provided. Measurements older than this will have
	 * smaller weight than <code>1/e</code>.
	 * @param window the exponential lapse window (number of measurements)
	 * @param factor a factor by which raw values are reduced during analysis; e.g. to analyze in ms and
	 * raw values are ns, set the factor to 1000000.0.
	 * @since 4.2
	 */
	public ExponentialMovingAverage(int window, double factor) {
		this.window = window;
		this.retention = window * 5; // last retained value contributes just 0.5% to the sum
		this.factor = factor;
	}

	public synchronized void reset() {
		this.count = 0;
		this.min = Double.MAX_VALUE;
		this.max = 0;
		this.samples.clear();
	}

	/**
	 * Add a new measurement to the series.
	 * @param value the measurement to append
	 */
	public synchronized void append(double value) {
		if (this.samples.size() == this.retention) {
			this.samples.poll();
		}
		this.samples.add(value);
		this.count++; //NOSONAR - false positive, we're synchronized
	}

	private Statistics calc() {
		List<Double> copy;
		long currentCount;
		synchronized (this) {
			copy = new ArrayList<Double>(this.samples);
			currentCount = this.count;
		}
		double sum = 0;
		double decay = 1 - 1. / this.window;
		double sumSquares = 0;
		double weight = 0;
		double currentMin = this.min;
		double currentMax = this.max;
		for (Double value : copy) {
			value /= this.factor;
			if (value > currentMax) {
				currentMax = value;
			}
			if (value < currentMin) {
				currentMin = value;
			}
			sum = decay * sum + value;
			sumSquares = decay * sumSquares + value * value;
			weight = decay * weight + 1;
		}
		synchronized (this) {
			if (currentMax > this.max) {
				this.max = currentMax;
			}
			if (currentMin < this.min) {
				this.min = currentMin;
			}
		}
		double mean = weight > 0 ? sum / weight : 0.;
		double var = weight > 0 ? sumSquares / weight - mean * mean : 0.;
		double standardDeviation =  var > 0 ? Math.sqrt(var) : 0;
		return new Statistics(currentCount, currentMin == Double.MAX_VALUE ? 0 : currentMin, currentMax, mean, standardDeviation); //NOSONAR
	}

	/**
	 * @return the number of measurements recorded
	 */
	public int getCount() {
		return (int) this.count;
	}

	/**
	 * @return the number of measurements recorded
	 */
	public long getCountLong() {
		return this.count;
	}

	/**
	 * @return the mean value
	 */
	public double getMean() {
		return calc().getMean();
	}

	/**
	 * @return the approximate standard deviation
	 */
	public double getStandardDeviation() {
		return calc().getStandardDeviation();
	}

	/**
	 * @return the maximum value recorded (not weighted)
	 */
	public double getMax() {
		return calc().getMax();
	}

	/**
	 * @return the minimum value recorded (not weighted)
	 */
	public double getMin() {
		return calc().getMin();
	}

	/**
	 * @return summary statistics (count, mean, standard deviation etc.)
	 */
	public Statistics getStatistics() {
		return calc();
	}

	@Override
	public String toString() {
		return getStatistics().toString();
	}

}
