package cz.muni.fi.lasaris.sbms.data.providers;

import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import cz.muni.fi.lasaris.sbms.data.entities.Address;
import cz.muni.fi.lasaris.sbms.data.entities.AggregationFunction;
import cz.muni.fi.lasaris.sbms.data.entities.Interpolation;
import cz.muni.fi.lasaris.sbms.data.entities.Sampling;
import cz.muni.fi.lasaris.sbms.data.entities.SeriesSpecs;
import cz.muni.fi.lasaris.sbms.data.entities.containers.Series;
import cz.muni.fi.lasaris.sbms.data.entities.containers.Slice;
import cz.muni.fi.lasaris.sbms.data.entities.containers.Trend;
import cz.muni.fi.lasaris.sbms.data.entities.containers.TrendGroup;
import cz.muni.fi.lasaris.sbms.data.entities.values.AggregatedValue;
import cz.muni.fi.lasaris.sbms.data.entities.values.InterpolatedValue;
import cz.muni.fi.lasaris.sbms.data.util.Aggregator;
import cz.muni.fi.lasaris.sbms.data.util.GroupHandler;
import cz.muni.fi.lasaris.sbms.data.util.GroupHandler.ContainerHandler;
import cz.muni.fi.lasaris.sbms.data.util.GroupHandler.ScalarHandler;
import cz.muni.fi.lasaris.sbms.data.util.Sampler;
import cz.muni.fi.lasaris.sbms.data.util.WindowMaker;

public abstract class AbstractSimpleTrendsProvider  implements TrendsProvider {
	
	
	
	protected abstract Map<Address, Trend> getRecords(List<Address> trends, ZonedDateTime from, ZonedDateTime to);
	protected abstract Map<Address, Trend> getClosestOlderRecord(List<Address> trends,ZonedDateTime timestamp);
	protected abstract Map<Address, Trend> getClosestNewerRecord(List<Address> trends,ZonedDateTime timestamp);
	
	@Override
	public Trend getTrend(Address trend, SeriesSpecs s) {
		 return getTrends(Arrays.asList(new Address[] {trend}), s).get(trend);
	}

	@Override
	public TrendGroup getTrends(List<Address> trends, SeriesSpecs s) {
		return new TrendGroup(getProcessedTrends(trends, s));
	}
	
	private Map<Address,Trend> getProcessedTrends(List<Address> trends, SeriesSpecs s) {
		return getProcessedTrends(trends, s, false);
	}
	
	private Map<Address,Trend> getProcessedTrends(List<Address> trends, SeriesSpecs s, boolean olderRequired) {
		if(s.isWithWindows()) {
			return WindowMaker.computeWindows(getRawTrends(trends, s, olderRequired), s);
		} else {
			return Sampler.computeSampling(getRawTrends(trends, s, olderRequired), s);
		}
	}
	
	private Map<Address,Trend> getRawTrends(List<Address> trends, SeriesSpecs s, boolean olderRequired) {
		if(s.getSampling() != Sampling.NONE && s.getInterpolation() == Interpolation.NONE
				|| s.getSampling() == Sampling.NONE && s.getInterpolation() != Interpolation.NONE)
		{
			throw new IllegalArgumentException("Both sampling and interpolation must be set");
		}
		Map<Address, Trend> results = getRecords(trends, s.getStart(), s.getEnd());
		if(s.getInterpolation() == Interpolation.NONE && s.getSampling() == Sampling.NONE) {
		// do nothing - we already have all the necessary data
			return results;
		}
		if (s.getInterpolation() == Interpolation.LAST_VALUE || s.getInterpolation() == Interpolation.LINEAR || olderRequired) {
			Map<Address, Trend> older = getClosestOlderRecord(trends, s.getStart());
			mergeSeriesMaps(results, older);
		}
		if (s.getInterpolation() == Interpolation.NEXT_VALUE || s.getInterpolation() == Interpolation.LINEAR) {
			Map<Address, Trend> newer = getClosestNewerRecord(trends, s.getStart());
			mergeSeriesMaps(results, newer);
		}
		return results;
	}
	
	
	@Override
	public Map<String, TrendGroup> getTrendsByGroup(Map<String, List<Address>>  trendGroups, SeriesSpecs s) {
		GroupHandler<Trend> g = new GroupHandler<Trend>();
		ContainerHandler<TrendGroup, Trend> p = new ContainerHandler<TrendGroup, Trend>() {
			@Override
			public TrendGroup getContainer() {
				return new TrendGroup();
			}

			@Override
			public Map<Address, Trend> getResults(List<Address> all) {
				return getProcessedTrends(all, s);
			}
			
		};
		return g.getGroups(trendGroups, p);
	}

	@Override
	public AggregatedValue getTrendAggregation(Address trend, SeriesSpecs seriesSpecs,
			AggregationFunction aggregation) {
		return getTrendAggregations(Arrays.asList(new Address[] {trend}), seriesSpecs, aggregation).get(trend);
	}

	@Override
	public Map<Address, AggregatedValue> getTrendAggregations(List<Address> trends, SeriesSpecs seriesSpecs,
			AggregationFunction aggregation) {
		
		return Aggregator.computeTemporalAggregations(getProcessedTrends(trends, seriesSpecs, isOlderRequired(aggregation)), aggregation, seriesSpecs);
	}
	
	@Override
	public AggregatedValue getTrendsAggregation(List<Address> trends, SeriesSpecs seriesSpecs,
			AggregationFunction aggregation) {
		return Aggregator.computeOverallTemporalAggregation(getProcessedTrends(trends, seriesSpecs, isOlderRequired(aggregation)), aggregation, seriesSpecs);
	}
	
	private boolean isOlderRequired(AggregationFunction a) {
		switch(a) {
			case TEMPORAL_AVG:
			case WEIGHTED_AVG:
			case WEIGHTED_TEMPORAL_AVG:
			case TEMPORAL_RATIO:
				return true;
			default:
				return false;
		}
	}
	
	@Override
	public Map<String, AggregatedValue> getGroupTrendAggregations(Map<String, List<Address>>  trends, SeriesSpecs seriesSpecs,
			AggregationFunction aggregation) {
		GroupHandler<Trend> g = new GroupHandler<Trend>();
		ScalarHandler<AggregatedValue, Trend> p = new ScalarHandler<AggregatedValue, Trend>() {

			@Override
			public Map<Address, Trend> getResults(List<Address> all) {
				return getProcessedTrends(all, seriesSpecs, isOlderRequired(aggregation));
			}

			@Override
			public AggregatedValue getScalar(Map<Address, Trend> data) {
				return Aggregator.computeOverallTemporalAggregation(data, aggregation, seriesSpecs);
			}
			
		};
		return g.getGroups(trends, p);
	}

	@Override
	public InterpolatedValue getTrendValue(Address trend, ZonedDateTime timestamp, Interpolation interpolation) {
		return getSlice(Arrays.asList(new Address[] {trend}), timestamp, interpolation).get(trend);
	}

	@Override
	public Slice getSlice(List<Address> trends, ZonedDateTime timestamp, Interpolation interpolation) {
		Map<Address, Trend> results = null;
		switch(interpolation) {
			case LAST_VALUE:
				results = getClosestOlderRecord(trends, timestamp);
				return new Slice(results, interpolation, timestamp);
			case NEXT_VALUE:
				results = getClosestNewerRecord(trends, timestamp);
				return new Slice(results, interpolation, timestamp);
			case LINEAR:
				results = getClosestOlderRecord(trends, timestamp);
				mergeSeriesMaps(results, getClosestNewerRecord(trends, timestamp));
				return new Slice(results, interpolation, timestamp);
			default:
				throw new IllegalArgumentException("Unsupported interpolation");
		}
	}

	@Override
	public Series<Slice> getSlices(List<Address> trends, SeriesSpecs seriesSpecs) {
		if(seriesSpecs.getSampling() == Sampling.NONE) {
			throw new IllegalArgumentException("Sampling must be set when creating slices.");
		}
		return Sampler.createSlices(getProcessedTrends(trends, seriesSpecs));
	}

	@Override
	public Series<AggregatedValue> getSliceAggregations(List<Address> trends, SeriesSpecs seriesSpecs,
			AggregationFunction aggregation) {
		Series<AggregatedValue> results = new Series<AggregatedValue>();
		Series<Slice> slices = getSlices(trends, seriesSpecs);
		for(ZonedDateTime d : slices.getKeys()) {
			AggregatedValue v = Aggregator.computeAggregation(slices.get(d).getData(), aggregation);
			results.add(d, v);
		}
		return results;
	}

	@Override
	public Series<Map<String, AggregatedValue>> getGroupSliceAggregations(Map<String, List<Address>> trends,
			SeriesSpecs seriesSpecs, AggregationFunction aggregation) {
		
		Series<Map<String, AggregatedValue>> results = new Series<Map<String, AggregatedValue>>();
		Map<String, TrendGroup> rawResults = getTrendsByGroup(trends, seriesSpecs);
		for(String group : rawResults.keySet()) {
			TrendGroup tg = rawResults.get(group);
			Series<Slice> slices = Sampler.createSlices(tg.getData());
			if(results.getKeys().size() == 0) {
				// empty result - we create the "slices"
				for(ZonedDateTime dt :slices.getKeys()) {
					results.add(dt, new LinkedHashMap<String, AggregatedValue>());
				}
			}
			for(ZonedDateTime dt : slices.getKeys()) {
				AggregatedValue v = Aggregator.computeAggregation(slices.get(dt).getData(), aggregation);
				results.get(dt).put(group, v);
			}
		}
		return results;
	}
	
	private <S, T> void mergeSeriesMaps(Map<T, ? extends Series<S>> destination, Map<T, ? extends Series<S>> source) {
		for(T key : destination.keySet()) {
			Series<S> series = source.get(key);
			for(ZonedDateTime seriesKey : series.getKeys())
			destination.get(key).add(seriesKey, series.get(seriesKey));
		}
	}

}
