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

import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;

import com.cronutils.model.Cron;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.model.time.ExecutionTime;
import com.cronutils.parser.CronParser;

import cz.muni.fi.lasaris.sbms.data.entities.Address;
import cz.muni.fi.lasaris.sbms.data.entities.DataType;
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.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.values.InterpolatedValue;
import cz.muni.fi.lasaris.sbms.data.entities.values.Value;

public class Sampler {
	
	public static Map<Address, Trend> computeSampling(Map<Address, Trend> data, SeriesSpecs s) {
		List<ZonedDateTime> timestamps = generateTimestamps(s);
		
		Map<Address, Trend> r = new LinkedHashMap<Address, Trend>(data.size());
		for(Address a : data.keySet()) {
			Trend ot = data.get(a);
			Trend rt = new Trend(getSamples(ot.getNavigableData(), timestamps, s.getInterpolation()));
			r.put(a, rt);
		}
		
		return data;
	}
	
	private static NavigableMap<ZonedDateTime, Value> getSamples(NavigableMap<ZonedDateTime, Value> d, List<ZonedDateTime> t, Interpolation interpolation) {
		if(d == null || d.isEmpty()) {
			return errorSamples(t, interpolation);
		}
		
		NavigableMap<ZonedDateTime, Value> r = new TreeMap<ZonedDateTime, Value>();
		
		Value olderValue = null;
		Value newerValue = d.firstEntry().getValue();
		ZonedDateTime olderTS = null;
		ZonedDateTime newerTS = d.firstKey();
		
		Iterator<Map.Entry<ZonedDateTime, Value>> i = d.entrySet().iterator();
		for(ZonedDateTime targetTS : t) {
			Map.Entry<ZonedDateTime, Value> e = null;
			
			// this loop gets us to situation when newerTS contains TS that is higher than target TS
			while(i.hasNext() && newerTS.compareTo(targetTS) <= 0) {
				olderValue = newerValue;
				olderTS = newerTS;
				e = i.next();
				newerValue = e.getValue();
				newerTS = e.getKey();
			}
			// olderValue might be null if it is the first record - that is handled in the getInterpolation method
			InterpolatedValue v = getInterpolation(olderTS, olderValue, newerTS, newerValue, interpolation, targetTS);
			r.put(targetTS, v);
		}
		return r;
	}
	
	private static NavigableMap<ZonedDateTime, Value> errorSamples(List<ZonedDateTime> t, Interpolation i) {
		NavigableMap<ZonedDateTime, Value> r = new TreeMap<ZonedDateTime, Value>();
		for(ZonedDateTime dt : t) {
			r.put(dt, InterpolatedValue.getInterpolationError(i, "No data to use for sampling"));
		}
		return r;
	}
	
	private static List<ZonedDateTime> generateTimestamps(SeriesSpecs s) {
		switch(s.getSampling()) {
		case DURATION:
			return generateDurationTimestamps(s);
		case CRON:
			return generateCRONTimestamps(s);
		case NONE:
		default:
			throw new IllegalArgumentException("Sampling must be set");
		}
	}
	
	private static List<ZonedDateTime> generateDurationTimestamps(SeriesSpecs s) {
		List<ZonedDateTime> r = new LinkedList<ZonedDateTime>();
		
		ZonedDateTime t = s.getStart();
		while (t.isBefore(s.getEnd()) || t.isEqual(s.getEnd())) {
			r.add(t);
			t = t.plus(s.getSamplingDuration());
		}
		
		return r;
	}
	
	private static List<ZonedDateTime> generateCRONTimestamps(SeriesSpecs s) {
		List<ZonedDateTime> r = new LinkedList<ZonedDateTime>();
		
		CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX));
		Cron expr = null;
		try {
			expr = parser.parse(s.getSamplingCron());
			expr.validate();
		} catch(Exception e) {
			r.add(s.getStart());
			r.add(s.getEnd());
			return r;
		}
		
		ZonedDateTime t = s.getStart();
		ExecutionTime et = ExecutionTime.forCron(expr);
		t = et.nextExecution(t);
		while (t.isBefore(s.getEnd()) || t.isEqual(s.getEnd())) {
			r.add(t);
			t = et.nextExecution(t);
		}
		
		return r;
	}
	
	
	/**
	 * Creates Slices from sampled trends. The trends must be previously sampled by the computeSampling method! If not called,
	 * the method will behave unpredictable - failing with NUllPointerException and mixing up data.
	 * This method only changes the structure of the data.
	 * @param data 
	 * @param s
	 * @return
	 */
	public static Series<Slice> createSlices(Map<Address, Trend> data) {
		if(data == null) {
			throw new IllegalArgumentException("no data to process");
		}
		Series<Slice> r = new Series<Slice>();
		if((data.size() == 0)) {
			return r;
		}
		Iterator<Address> i = data.keySet().iterator();
		Address a = i.next();
		// first trend - creates the slices during the creation
		Trend t = data.get(a);
		for(ZonedDateTime dt : t.getKeys()) {
			Slice s = new Slice();
			s.add(a, ((InterpolatedValue)t.get(dt)));
			r.add(dt, s);
		}
		
		// looping through trends
		while(i.hasNext()) {
			a = i.next();
			t = data.get(a);
			// using iterator might be little faster than calling get for each ZonedDateTime from original trend
			Iterator<Slice> i2 = r.getData().values().iterator();
			// we rely on sampling and do not perform any other checks
			// looping through records
			for(ZonedDateTime dt : t.getKeys()) {
				Slice s = i2.next();
				s.add(a, (InterpolatedValue)t.get(dt));
			}
		}
		
		return r;
	}

	public static InterpolatedValue getInterpolation(ZonedDateTime sTS, Value sVal, ZonedDateTime eTS, Value eVal,
			Interpolation i, ZonedDateTime tTS) {
		DataType dt = null;
		Object val = null;
		switch(i) {
		
		case LINEAR:
			if(eVal == null) {
				return InterpolatedValue.getInterpolationError(i, "No end data");
			}
			if(sVal == null) {
				return InterpolatedValue.getInterpolationError(i, "No start data");
			}
			if(eTS == null) {
				return InterpolatedValue.getInterpolationError(i, "No end timestamp");
			}
			if(sTS == null) {
				return InterpolatedValue.getInterpolationError(i, "No start timestamp");
			}
			if(tTS == null) {
				return InterpolatedValue.getInterpolationError(i, "No target timestamp");
			}
			if(eVal.getDataType() != sVal.getDataType()) {
				return InterpolatedValue.getInterpolationError(i, "data types are not the same");
			}
			if(sTS.isEqual(eTS)) {
				return InterpolatedValue.getInterpolationError(i, "dates are the same");
			}
			if(sTS.isAfter(eTS)) {
				return InterpolatedValue.getInterpolationError(i, "dates are not in the correct order");
			}
			if(sTS.isAfter(tTS) || eTS.isBefore(tTS)) {
				return InterpolatedValue.getInterpolationError(i, "target timestamp is not between the other two timestamps");
			}
			dt = eVal.getDataType();
			if(!(dt == DataType.REAL || dt == DataType.INTEGER)) {
				throw new IllegalArgumentException("unsupported data type");
			}
			double x1 = 0;
			double x2 = 0;
			if(dt == DataType.INTEGER) {
				x1 = ((Integer)sVal.getValue()).doubleValue();
				x2 = ((Integer)sVal.getValue()).doubleValue();
			}
			if(dt == DataType.REAL) {
				x1 = ((Double)sVal.getValue()).doubleValue();
				x2 = ((Double)sVal.getValue()).doubleValue();
			}
			
			long period = (eTS.getLong(ChronoField.INSTANT_SECONDS) * 1000 + eTS.getLong(ChronoField.MILLI_OF_SECOND))
							- (sTS.getLong(ChronoField.INSTANT_SECONDS) * 1000 + sTS.getLong(ChronoField.MILLI_OF_SECOND));
			long target = (tTS.getLong(ChronoField.INSTANT_SECONDS) * 1000 + tTS.getLong(ChronoField.MILLI_OF_SECOND))
					- (sTS.getLong(ChronoField.INSTANT_SECONDS) * 1000 + sTS.getLong(ChronoField.MILLI_OF_SECOND));
			double incrementPerMilli = (x2 - x1) / period;
			val = Double.valueOf(target * incrementPerMilli);
			dt = DataType.REAL;
			return new InterpolatedValue(val, dt, i);
		
		case NEXT_VALUE:
			if(eVal == null) {
				return InterpolatedValue.getInterpolationError(i, "No data");
			}
			val = eVal.getValue();
			dt = eVal.getDataType();
			return new InterpolatedValue(val, dt, i);
			
		case LAST_VALUE:
		case NONE:
		default:
			if(sVal == null) {
				return InterpolatedValue.getInterpolationError(i, "No data");
			}
			val = sVal.getValue();
			dt = sVal.getDataType();
			return new InterpolatedValue(val, dt, i);
		}
		
		
	}
}
