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

import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.log4j.Logger;

import cz.muni.fi.lasaris.sbms.data.api.request.TrendRequest;
import cz.muni.fi.lasaris.sbms.data.entities.Address;
import cz.muni.fi.lasaris.sbms.data.entities.Interpolation;
import cz.muni.fi.lasaris.sbms.data.entities.SeriesSpecs;
import cz.muni.fi.lasaris.sbms.data.entities.containers.Aggregation;
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.providers.AbstractSimpleTrendsProvider;
import cz.muni.fi.lasaris.sbms.data.providers.TrendsProvider;

public class TrendsDataCollector {

	final static Logger logger = Logger.getLogger(TrendsDataCollector.class);
	
	public Trend getTrend(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getTrend(r.getAddress(), r.getSeriesSpecs());
	}

	public InterpolatedValue getValue(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getTrendValue(r.getAddress(), r.getSeriesSpecs().getStart(), r.getSeriesSpecs().getInterpolation());
	}

	public AggregatedValue getAggregate(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getTrendAggregation(r.getAddress(), r.getSeriesSpecs(), r.getAggregation());
	}

	public Slice getSlice(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getSlice(r.getAddressSpecs().getAddresses(), r.getSeriesSpecs().getStart(), r.getSeriesSpecs().getInterpolation());
	}

	public Map<String, AggregatedValue> getGroupedAggregatesForSlice(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getGroupedSliceAggregations(r.getGroupedAddressSpecs().getGroups(), r.getSeriesSpecs().getStart(), 
				r.getSeriesSpecs().getInterpolation(), r.getAggregation());
	}

	public AggregatedValue getAggregateForSlice(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getSliceAggregation(r.getAddressSpecs().getAddresses(), r.getSeriesSpecs().getStart(), 
				r.getSeriesSpecs().getInterpolation(), r.getAggregation());
	}

	public Map<String, TrendGroup> getGroupedTrends(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getTrendsByGroup(r.getGroupedAddressSpecs().getGroups(), r.getSeriesSpecs());
	}

	public Map<Address, Trend> getTrends(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getTrends(r.getAddressSpecs().getAddresses(), r.getSeriesSpecs()).getData();
	}

	public Trend getSeriesOfAggregates(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getSliceAggregationSeries(r.getAddressSpecs().getAddresses(), r.getSeriesSpecs(), r.getAggregation());
	}

	public Map<String, Trend> getGroupedSeriesOfAggregates(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getGroupedSliceAggregationsSeries(r.getGroupedAddressSpecs().getGroups(), r.getSeriesSpecs(), r.getAggregation());
	}

	public Map<String, Map<Address, AggregatedValue>> getGroupedAggregates(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getGroupedTrendAggregations(r.getGroupedAddressSpecs().getGroups(), r.getSeriesSpecs(), r.getAggregation());
	}

	public Aggregation<Address> getAggregates(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return new Aggregation<Address>(p.getTrendAggregations(r.getAddressSpecs().getAddresses(), r.getSeriesSpecs(), r.getAggregation()));
	}

	public Map<String, AggregatedValue> getGroupedTrendsAggregate(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getGroupTrendAggregations(r.getGroupedAddressSpecs().getGroups(), r.getSeriesSpecs(), r.getAggregation());
	}

	public AggregatedValue getTrendsAggregate(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getTrendsAggregation(r.getAddressSpecs().getAddresses(), r.getSeriesSpecs(), r.getAggregation());
	}

	public Map<String, Series<Slice>> getGroupedSeriesOfSlices(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getGroupedSlices(r.getGroupedAddressSpecs().getGroups(), r.getSeriesSpecs());
	}

	public Series<Slice> getSeriesOfSlices(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getSlices(r.getAddressSpecs().getAddresses(), r.getSeriesSpecs());
	}

	public Map<String, Trend> getGroupedWindowedTrendsAggregate(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getGroupedSliceAggregationsSeries(r.getGroupedAddressSpecs().getGroups(), r.getSeriesSpecs(), r.getSeriesSpecs().getWindowAggregation());
	}

	public Trend getWindowedTrendsAggregate(TrendRequest r) {
		TrendsProvider p = getProvider(r);
		return p.getSliceAggregationSeries(r.getAddressSpecs().getAddresses(), r.getSeriesSpecs(), r.getSeriesSpecs().getWindowAggregation());
	}
	
	
	private TrendsProvider getProvider(TrendRequest r) {
		ProtocolParser provider = new ProtocolParser(r);
		return new AbstractSimpleTrendsProvider() {
			
			private ProtocolParser p = provider;
			
			@Override
			protected Map<Address,Trend> getProcessedTrends(List<Address> trends, SeriesSpecs s, boolean olderRequired) {
				NavigableSet<String> protocols = p.getProtocolsSet();
				
				if(protocols.size() == 1) {
					return ProviderManager.getTrendsProvider(protocols.first())
							.getTrends(p.getAddresses(protocols.first()), s, olderRequired).getData();
				}

				AtomicInteger counter = new AtomicInteger(0);
				int count = protocols.size();

				Map<Address,Trend> result = Collections.synchronizedMap(new LinkedHashMap<Address,Trend>());

				for(String protocol : protocols) {
					new Thread(new Runnable() {
						@Override
						public void run() {
							Map<Address,Trend> rs = ProviderManager.getTrendsProvider(protocol)
									.getTrends(p.getAddresses(protocol), s, olderRequired).getData(); 
							synchronized(result) {
								result.putAll(rs);;
								counter.incrementAndGet();
								result.notifyAll();
							}

						}}).start();
				}
				synchronized(result) {
					while(counter.get() < count) {
						try {
							result.wait();
						} catch (InterruptedException e) {
							logger.error("Exception while waiting for completion.", e);
						}
					}
				}

				return result;
			}
			
			@Override
			public Slice getSlice(List<Address> trends, ZonedDateTime timestamp, Interpolation interpolation) {
				NavigableSet<String> protocols = p.getProtocolsSet();
				
				if(protocols.size() == 1) {
					return ProviderManager.getTrendsProvider(protocols.first())
							.getSlice(p.getAddresses(protocols.first()), timestamp, interpolation);
				}

				AtomicInteger counter = new AtomicInteger(0);
				int count = protocols.size();

				Slice result = new Slice();

				for(String protocol : protocols) {
					new Thread(new Runnable() {
						@Override
						public void run() {
							Slice rs = ProviderManager.getTrendsProvider(protocol)
									.getSlice(p.getAddresses(protocol), timestamp, interpolation);
							synchronized(result) {
								result.addAll(rs.getData());
								counter.incrementAndGet();
								result.notifyAll();
							}

						}}).start();
				}
				synchronized(result) {
					while(counter.get() < count) {
						try {
							result.wait();
						} catch (InterruptedException e) {
							logger.error("Exception while waiting for completion.", e);
						}
					}
				}

				return result;
			}
			
			// UNUSED methods follow
			
			@Override
			public void init(Properties props, String name, String propertiesPrefix) {
			}

			@Override
			public void close() {
			}

			@Override
			protected Map<Address, Trend> getRecords(List<Address> trends, ZonedDateTime from, ZonedDateTime to) {
				// will never be called
				return null;
			}

			@Override
			protected Map<Address, Trend> getClosestOlderRecord(List<Address> trends, ZonedDateTime timestamp) {
				// will never be called
				return null;
			}

			@Override
			protected Map<Address, Trend> getClosestNewerRecord(List<Address> trends, ZonedDateTime timestamp) {
				// will never be called
				return null;
			}
			
			
		};
	}
}
