/*
 * Copyright 2012-2018 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.boot.actuate.metrics.rich;

import org.springframework.util.Assert;

/**
 * A gauge which stores the maximum, minimum and average in addition to the current value.
 * <p>
 * The value of the average will depend on whether a weight ('alpha') is set for the
 * gauge. If it is unset, the average will contain a simple arithmetic mean. If a weight
 * is set, an exponential moving average will be calculated as defined in this
 * <a href="https://www.itl.nist.gov/div898/handbook/pmc/section4/pmc431.htm">NIST
 * document</a>.
 *
 * @author Luke Taylor
 */
public final class RichGauge {

	/**
	 * The suffix for count gauges.
	 */
	public static final String COUNT = ".count";

	/**
	 * The suffix for max gauges.
	 */
	public static final String MAX = ".max";

	/**
	 * The suffix for min gauges.
	 */
	public static final String MIN = ".min";

	/**
	 * The suffix for average value gauges.
	 */
	public static final String AVG = ".avg";

	/**
	 * The suffix for alpha gauges.
	 */
	public static final String ALPHA = ".alpha";

	/**
	 * The suffix for value gauges.
	 */
	public static final String VAL = ".val";

	private final String name;

	private double value;

	private double average;

	private double max;

	private double min;

	private long count;

	private double alpha;

	/**
	 * Creates an "empty" gauge. The average, max and min will be zero, but this initial
	 * value will not be included after the first value has been set on the gauge.
	 * @param name the name under which the gauge will be stored.
	 */
	public RichGauge(String name) {
		this(name, 0.0);
		this.count = 0;
	}

	public RichGauge(String name, double value) {
		Assert.notNull(name, "The gauge name cannot be null or empty");
		this.name = name;
		this.value = value;
		this.average = this.value;
		this.min = this.value;
		this.max = this.value;
		this.alpha = -1.0;
		this.count = 1;
	}

	public RichGauge(String name, double value, double alpha, double mean, double max,
			double min, long count) {
		this.name = name;
		this.value = value;
		this.alpha = alpha;
		this.average = mean;
		this.max = max;
		this.min = min;
		this.count = count;
	}

	/**
	 * Return the name of the gauge.
	 * @return the name
	 */
	public String getName() {
		return this.name;
	}

	/**
	 * Return the current value of the gauge.
	 * @return the value
	 */
	public double getValue() {
		return this.value;
	}

	/**
	 * Return either an exponential weighted moving average or a simple mean,
	 * respectively, depending on whether the weight 'alpha' has been set for this gauge.
	 * @return the average over all the accumulated values
	 */
	public double getAverage() {
		return this.average;
	}

	/**
	 * Return the maximum value of the gauge.
	 * @return the maximum value
	 */
	public double getMax() {
		return this.max;
	}

	/**
	 * Return the minimum value of the gauge.
	 * @return the minimum value
	 */
	public double getMin() {
		return this.min;
	}

	/**
	 * Return the number of times the value has been set.
	 * @return the value set count
	 */
	public long getCount() {
		return this.count;
	}

	/**
	 * Return the smoothing constant value.
	 * @return the alpha smoothing value
	 */
	public double getAlpha() {
		return this.alpha;
	}

	public RichGauge setAlpha(double alpha) {
		Assert.isTrue(alpha == -1 || (alpha > 0.0 && alpha < 1.0),
				"Smoothing constant must be between 0 and 1, or -1 to use arithmetic mean");
		this.alpha = alpha;
		return this;
	}

	RichGauge set(double value) {
		if (this.count == 0) {
			this.max = value;
			this.min = value;
		}
		else if (value > this.max) {
			this.max = value;
		}
		else if (value < this.min) {
			this.min = value;
		}

		if (this.alpha > 0.0 && this.count > 0) {
			this.average = this.alpha * this.value + (1 - this.alpha) * this.average;
		}
		else {
			double sum = this.average * this.count;
			sum += value;
			this.average = sum / (this.count + 1);
		}
		this.count++;
		this.value = value;
		return this;
	}

	RichGauge reset() {
		this.value = 0.0;
		this.max = 0.0;
		this.min = 0.0;
		this.average = 0.0;
		this.count = 0;
		return this;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}

		RichGauge richGauge = (RichGauge) o;

		if (!this.name.equals(richGauge.name)) {
			return false;
		}

		return true;
	}

	@Override
	public int hashCode() {
		return this.name.hashCode();
	}

	@Override
	public String toString() {
		return "Gauge [name = " + this.name + ", value = " + this.value + ", alpha = "
				+ this.alpha + ", average = " + this.average + ", max = " + this.max
				+ ", min = " + this.min + ", count = " + this.count + "]";
	}

}
