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

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
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.DataType;
import cz.muni.fi.lasaris.sbms.data.entities.values.AggregatedValue;
import cz.muni.fi.lasaris.sbms.data.entities.values.RawValue;
import cz.muni.fi.lasaris.sbms.data.entities.values.Value;

public class Aggregations {
	public static AggregationFunction getAggregation(String agg) {
		switch(agg.toUpperCase().trim()) {
		case "COUNT":
			return AggregationFunction.COUNT;
		case "MAX":
			return AggregationFunction.MAX;
		case "MIN":
			return AggregationFunction.MIN;
		case "AVG":
			return AggregationFunction.AVG;
		case "MEDIAN":
			return AggregationFunction.MEDIAN;
		case "SUM":
			return AggregationFunction.SUM;
		case "TEMPORAL_AVG":
			return AggregationFunction.TEMPORAL_AVG;
		case "WEIGHTED_AVG":
			return AggregationFunction.WEIGHTED_AVG;
		case "WEIGHTED_TEMPORAL_AVG":
			return AggregationFunction.WEIGHTED_TEMPORAL_AVG;
		case "NONE":
			return AggregationFunction.NONE;
		default:
			throw new IllegalArgumentException("Unknown aggregation type.");
		}
		
	}

	public static AggregatedValue computeAggregation(Map<Address, RawValue> data, AggregationFunction aggregation) {
		if(data == null) {
			throw new NullPointerException("data");
		}
		switch(aggregation) {
		case AVG:
			return avg(data);
		case WEIGHTED_AVG:
			return weightedAvg(data);
		case COUNT:
			return new AggregatedValue(data.size(), aggregation);
		case MEDIAN:
			return median(data);
		case MIN:
			return min(data);
		case MAX:
			return max(data);
		case SUM:
			return sum(data);
		case AND:
			return and(data);
		case OR:
			return or(data);
		case RATIO:
			return ratio(data);
		case NONE:
		case TEMPORAL_AVG:
		case WEIGHTED_TEMPORAL_AVG:
		default:
			return AggregatedValue.getAggregationError(aggregation, "Aggregation not supported for a Snapshot");
			
		}
	}

	private static AggregatedValue weightedAvg(Map<Address, ? extends Value> data) {
		// it could be possible to compute this using linear algebra (matrix operations)
		// however, we do not expect such large number of weights and data points to make significant difference
		double sum = 0;
		double count = 0;
		for(Map.Entry<Address,? extends Value> e : data.entrySet()) {
			Address a = e.getKey();
			Value v = e.getValue();
			
			DataType d = v.getDataType();
			double val = 0;
			
			double weight = 1;
			if(a.getWeights() != null) {
				for(int i = 0; i < a.getWeights().length; i++) {
					weight *= a.getWeights()[i];
				}
			}
			if(d == DataType.REAL) {	
				val = ((Double)v.getValue()).doubleValue();
			} else if(d == DataType.INTEGER) {
				val = ((Long)v.getValue()).intValue();
			} else {
				continue;
			}
			sum += val * weight;
			count += weight;
		}
		
		if(count == 0) {
			return AggregatedValue.getAggregationError(AggregationFunction.WEIGHTED_AVG, "No data of supported data types provided.");		
		} else {
			return new AggregatedValue(sum / count, DataType.REAL, AggregationFunction.WEIGHTED_AVG);
		}
	}

	private static AggregatedValue avg(Map<Address, ? extends Value> data) {
		double sum = 0;
		int count = 0;
		for(Value v : data.values()) {
			DataType d = v.getDataType();
			double val = 0;
			if(d == DataType.REAL) {	
				val = ((Double)v.getValue()).doubleValue();
			} else if(d == DataType.INTEGER) {
				val = ((Long)v.getValue()).intValue();
			} else {
				continue;
			}
			sum += val;
			count += 1;
			
		}
		if(count == 0) {
			return AggregatedValue.getAggregationError(AggregationFunction.AVG, "No data of supported data types provided.");		
		} else {
			return new AggregatedValue(sum / count, DataType.REAL, AggregationFunction.AVG);
		}
	}

	private static AggregatedValue median(Map<?, ? extends Value> data) {
		List<Value> filtered = new ArrayList<Value>(data.size());
		for(Value v : data.values()) {
			if(v.getDataType() == DataType.INTEGER || v.getDataType() == DataType.REAL) {
				filtered.add(v);
			}
		}
		
		if(filtered.size() == 0) {
			return AggregatedValue.getAggregationError(AggregationFunction.MEDIAN, "No data of supported data types provided.");	
		}
		
		Collections.sort(filtered, new Comparator<Value>() {
			private double getVal(Value v) {
				if(v.getDataType() == DataType.INTEGER) {
					return ((Long)v.getValue()).doubleValue();
				}
				if(v.getDataType() == DataType.REAL) {
					return ((Double)v.getValue()).doubleValue();
				}
				return 0;
			}
			
			@Override
			public int compare(Value o1, Value o2) {
				return Double.compare(getVal(o1), getVal(o2));
			}});
		Value median = filtered.get(filtered.size() / 2);
		return new AggregatedValue(median.getValue(), median.getDataType(), AggregationFunction.MEDIAN);
	}

	private static AggregatedValue max(Map<?, ? extends Value> data) {
		double max = Double.NEGATIVE_INFINITY;
		boolean hasFloat = false;
		for(Value v : data.values()) {
			DataType d = v.getDataType();
			double val = Double.NEGATIVE_INFINITY;
			if(d == DataType.REAL) {
				hasFloat = true;
				val = ((Double)v.getValue()).doubleValue();
			}
			if(d == DataType.INTEGER) {
				val = ((Long)v.getValue()).intValue();
			}
			/*
			if(d == DataType.BOOL) {
				val = (((Boolean)v.getValue()).booleanValue()) ? 1 : 0;
			}
			*/
			if(val > max) {
				max = val;
			}
		}
		if(max == Double.NEGATIVE_INFINITY) {
			return AggregatedValue.getAggregationError(AggregationFunction.MAX, "No data of supported data types provided.");	
		} else if (hasFloat) {
			return new AggregatedValue(max, DataType.REAL, AggregationFunction.MAX);	
		} else {
			return new AggregatedValue((long)max, DataType.INTEGER, AggregationFunction.MAX);
		}
	}
	
	private static AggregatedValue min(Map<?, ? extends Value> data) {
		double min = Double.POSITIVE_INFINITY;
		boolean hasFloat = false;
		for(Value v : data.values()) {
			DataType d = v.getDataType();
			double val = Double.POSITIVE_INFINITY;
			if(d == DataType.REAL) {
				hasFloat = true;
				val = ((Double)v.getValue()).doubleValue();
			}
			if(d == DataType.INTEGER) {
				val = ((Long)v.getValue()).intValue();
			}
			/*
			if(d == DataType.BOOL) {
				val = (((Boolean)v.getValue()).booleanValue()) ? 1 : 0;
			}
			*/
			if(val < min) {
				min = val;
			}
		}
		if(min == Double.POSITIVE_INFINITY) {
			return AggregatedValue.getAggregationError(AggregationFunction.MIN, "No data of supported data types provided.");	
		} else if (hasFloat) {
			return new AggregatedValue(min, DataType.REAL, AggregationFunction.MIN);	
		} else {
			return new AggregatedValue((long)min, DataType.INTEGER, AggregationFunction.MIN);
		}
	}
	
	private static AggregatedValue sum(Map<?, ? extends Value> data) {
		
		double sum = 0;
		boolean hasFloat = false;
		boolean hasInt = false;
		for(Value v : data.values()) {
			DataType d = v.getDataType();
			if(d == DataType.REAL) {
				hasFloat = true;
				sum += ((Double)v.getValue()).doubleValue();
			}
			if(d == DataType.INTEGER) {
				hasInt = true;
				sum += ((Long)v.getValue()).intValue();
			}
			/*
			if(d == DataType.BOOL) {
				if(((Boolean)v.getValue()).booleanValue()) {
					sum += 1;
				}
			}
			*/
		}
		
		if(hasFloat) {
			return new AggregatedValue(sum, AggregationFunction.SUM);
		} else if(hasInt) {
			return new AggregatedValue((long)sum, AggregationFunction.SUM);
		} else {
			return AggregatedValue.getAggregationError(AggregationFunction.SUM, "No data of supported data types provided.");	
		}
	}
	
	private static AggregatedValue and(Map<?, ? extends Value> data) {
		
		boolean r = true;
		boolean has = false;
		for(Value v : data.values()) {
			DataType d = v.getDataType();
			
			if(d == DataType.BOOL) {
				has = true;
				r = r && (((Boolean)v.getValue()).booleanValue()); 
			}
			
		}
		
		if(!has) {
			return AggregatedValue.getAggregationError(AggregationFunction.AND, "No data of supported data types provided.");		
		} else {
			return new AggregatedValue(r, AggregationFunction.AND);
		}
	}
	
	private static AggregatedValue or(Map<?, ? extends Value> data) {
		
		boolean r = false;
		boolean has = false;
		for(Value v : data.values()) {
			DataType d = v.getDataType();
			
			if(d == DataType.BOOL) {
				has = true;
				r = r || (((Boolean)v.getValue()).booleanValue()); 
			}
			
		}
		if(!has) {
			return AggregatedValue.getAggregationError(AggregationFunction.OR, "No data of supported data types provided.");		
		} else {
			return new AggregatedValue(r, AggregationFunction.OR);
		}
	}
	
	private static AggregatedValue ratio(Map<Address, RawValue> data) {
		int count = 0;
		int t = 0;
		for(Value v : data.values()) {
			DataType d = v.getDataType();
			
			if(d == DataType.BOOL) {
				count += 1;
				if(((Boolean)v.getValue()).booleanValue()) {
					t += 1;
				}; 
			}
			
		}
		if(count == 0) {
			return AggregatedValue.getAggregationError(AggregationFunction.RATIO, "No data of supported data types provided.");		
		} else {
			return new AggregatedValue(((double)t) / count, DataType.REAL, AggregationFunction.RATIO);
		}
	}
}
