/*
 * Copyright 2012-2017 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
 *
 *      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 org.springframework.boot.actuate.metrics.export;

import java.io.Flushable;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.boot.actuate.metrics.Metric;
import org.springframework.boot.actuate.metrics.reader.MetricReader;
import org.springframework.boot.actuate.metrics.writer.CompositeMetricWriter;
import org.springframework.boot.actuate.metrics.writer.CounterWriter;
import org.springframework.boot.actuate.metrics.writer.Delta;
import org.springframework.boot.actuate.metrics.writer.GaugeWriter;
import org.springframework.boot.actuate.metrics.writer.MetricWriter;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.ReflectionUtils;

/**
 * {@link Exporter} that "exports" by copying metric data from a source
 * {@link MetricReader} to a destination {@link MetricWriter}. Actually the output writer
 * can be a {@link GaugeWriter}, in which case all metrics are simply output as their
 * current value. If the output writer is also a {@link CounterWriter} then metrics whose
 * names begin with "counter." are special: instead of writing them out as simple gauges
 * the writer will increment the counter value. This involves the exporter storing the
 * previous value of the counter so the delta can be computed. For best results with the
 * counters, do not use the exporter concurrently in multiple threads (normally it will
 * only be used periodically and sequentially, even if it is in a background thread, and
 * this is fine).
 *
 * @author Dave Syer
 * @since 1.3.0
 */
public class MetricCopyExporter extends AbstractMetricExporter {

	private static final Log logger = LogFactory.getLog(MetricCopyExporter.class);

	private final MetricReader reader;

	private final GaugeWriter writer;

	private final CounterWriter counter;

	private ConcurrentMap<String, Long> counts = new ConcurrentHashMap<>();

	private String[] includes = new String[0];

	private String[] excludes = new String[0];

	/**
	 * Create a new {@link MetricCopyExporter} instance.
	 * @param reader the metric reader
	 * @param writer the metric writer
	 */
	public MetricCopyExporter(MetricReader reader, GaugeWriter writer) {
		this(reader, writer, "");
	}

	/**
	 * Create a new {@link MetricCopyExporter} instance.
	 * @param reader the metric reader
	 * @param writer the metric writer
	 * @param prefix the name prefix
	 */
	public MetricCopyExporter(MetricReader reader, GaugeWriter writer, String prefix) {
		super(prefix);
		this.reader = reader;
		this.writer = writer;
		if (writer instanceof CounterWriter) {
			this.counter = (CounterWriter) writer;
		}
		else {
			this.counter = null;
		}
	}

	/**
	 * Set the include patterns used to filter metrics.
	 * @param includes the include patterns
	 */
	public void setIncludes(String... includes) {
		if (includes != null) {
			this.includes = includes;
		}
	}

	/**
	 * Set the exclude patterns used to filter metrics.
	 * @param excludes the exclude patterns
	 */
	public void setExcludes(String... excludes) {
		if (excludes != null) {
			this.excludes = excludes;
		}
	}

	@Override
	protected Iterable<Metric<?>> next(String group) {
		if (ObjectUtils.isEmpty(this.includes) && ObjectUtils.isEmpty(this.excludes)) {
			return this.reader.findAll();
		}
		return new PatternMatchingIterable(MetricCopyExporter.this.reader);
	}

	@Override
	protected void write(String group, Collection<Metric<?>> values) {
		for (Metric<?> value : values) {
			if (value.getName().startsWith("counter.") && this.counter != null) {
				this.counter.increment(calculateDelta(value));
			}
			else {
				this.writer.set(value);
			}
		}
	}

	private Delta<?> calculateDelta(Metric<?> value) {
		long delta = value.getValue().longValue();
		Long old = this.counts.replace(value.getName(), delta);
		if (old != null) {
			delta = delta - old;
		}
		else {
			this.counts.putIfAbsent(value.getName(), delta);
		}
		return new Delta<>(value.getName(), delta, value.getTimestamp());
	}

	@Override
	public void flush() {
		flush(this.writer);
	}

	private void flush(GaugeWriter writer) {
		if (writer instanceof CompositeMetricWriter) {
			for (MetricWriter child : (CompositeMetricWriter) writer) {
				flush(child);
			}
		}
		try {
			if (ClassUtils.isPresent("java.io.Flushable", null)) {
				if (writer instanceof Flushable) {
					((Flushable) writer).flush();
					return;
				}
			}
			Method method = ReflectionUtils.findMethod(writer.getClass(), "flush");
			if (method != null) {
				ReflectionUtils.invokeMethod(method, writer);
			}
		}
		catch (Exception ex) {
			logger.warn("Could not flush MetricWriter: " + ex.getClass() + ": "
					+ ex.getMessage());
		}
	}

	private class PatternMatchingIterable implements Iterable<Metric<?>> {

		private final MetricReader reader;

		PatternMatchingIterable(MetricReader reader) {
			this.reader = reader;
		}

		@Override
		public Iterator<Metric<?>> iterator() {
			return new PatternMatchingIterator(this.reader.findAll().iterator());
		}

	}

	private class PatternMatchingIterator implements Iterator<Metric<?>> {

		private Metric<?> buffer = null;

		private Iterator<Metric<?>> iterator;

		PatternMatchingIterator(Iterator<Metric<?>> iterator) {
			this.iterator = iterator;
		}

		@Override
		public boolean hasNext() {
			if (this.buffer != null) {
				return true;
			}
			this.buffer = findNext();
			return this.buffer != null;
		}

		private Metric<?> findNext() {
			while (this.iterator.hasNext()) {
				Metric<?> metric = this.iterator.next();
				if (isMatch(metric)) {
					return metric;
				}
			}
			return null;
		}

		private boolean isMatch(Metric<?> metric) {
			String[] includes = MetricCopyExporter.this.includes;
			String[] excludes = MetricCopyExporter.this.excludes;
			String name = metric.getName();
			if (ObjectUtils.isEmpty(includes)
					|| PatternMatchUtils.simpleMatch(includes, name)) {
				return !PatternMatchUtils.simpleMatch(excludes, name);
			}
			return false;
		}

		@Override
		public Metric<?> next() {
			Metric<?> metric = this.buffer;
			this.buffer = null;
			return metric;
		}

	};

}
