package be.ac.ulb.code.wit.rangeparser;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class RangeMatcher<T extends Range> {
	private List<T> ranges = new ArrayList<T>();
	private RangeParser<T> rangeParser;
	
	public RangeMatcher(RangeParser<T> rangeParser) {
		this.rangeParser = rangeParser;
	}
	
	/**
	 * Register a new media type supported by the application
	 * @param type The media-type being supported (along with optional parameters)
	 */
	public void registerKnownType(T range) {
		ranges.add(range);
	}
	/**
	 * Register a new media type supported by the application
	 * @param type The media-type being supported (along with optional parameters)
	 * @throws ParseException Occurs when an invalid media type is passed.
	 */
	public void registerKnownType(String range) throws ParseException {
		registerKnownType(rangeParser.parse(range));
		
	}
	
	/**
	 * Ensures that two sets of parameters are identical
	 */
	private boolean CheckParameters(List<Parameter> params1, List<Parameter> params2) {
		return (params1.size() == params2.size()) && 
			(params1.containsAll(params2)); 
	}
	
	/**
	 * Transforms the specified media query into a map, from the media type to MediaRange objects
	 * @param query The media query, as it appears in the Accept field of an HTTP request
	 * @return A map from media type into corresponding media ranges objects
	 * @throws ParseException Occurs if the media query is malformed.
	 */
	private Map<String, List<T>> parseMediaQueryAsMap(String query) throws ParseException {
		List<T> queryRanges = rangeParser.parseMediaQuery(query);
		Map<String, List<T>> queryRangesMap = 
			new HashMap<String, List<T>>();
		
		for (T queryRange: queryRanges) {
			String rangeType = queryRange.getTag();
			List<T> ranges = queryRangesMap.get(rangeType);
			
			if (ranges == null) {
				ranges = new ArrayList<T>();
				queryRangesMap.put(rangeType, ranges);
			}
			
			ranges.add(queryRange);
		}
		
		return queryRangesMap;
	}
	
	/**
	 * Determine the quality of a given application-supported MediaRange,
	 * with respect to a parsed user media query.
	 */
	private Double getRangeQuality(T range, Map<String, List<T>> query) {
		List<String> rangeTypes = new ArrayList<String>();
		rangeTypes.add(range.getTag());
		rangeTypes.addAll(range.getSubstitutions());
		double quality = -1.0;
		
		//Check for more specific types first, then
		// if none match, downgrade to a less specific type
		for (String rangeType: rangeTypes) {
			List<T> candidates = query.get(rangeType);
			if (candidates != null) {
				for (T mr: candidates) {
					if (this.CheckParameters(mr.getMediaParameters(), 
							range.getMediaParameters())) {
						quality = Math.max(quality, mr.getQuality()); 
					}
				}
				
				if (quality >= 0.0) break;
			}
		}
		
		return (quality >= 0.0 ? quality : null);
	}
		
	/**
	 * Get the best application-supported media type with respect to 
	 * an user posed media query, among all application-supported media types
	 * @param query The user posed media query, used to determine the best media type.
	 * @return The best media type, as a MediaRange object.
	 * @throws ParseException Occurs if the media query is malformed.
	 */
	public T getBestMatch(String query) throws ParseException {
		if (query == null) { 
			return this.ranges.get(0);
		}
		
		Map<String, List<T>> rangesMaps = parseMediaQueryAsMap(query);
		Double maxQuality = -1.0;
		T bestMatch = null;
		
		for (T range: this.ranges) {
			Double quality = getRangeQuality(range, rangesMaps);
			
			if (quality != null && quality > maxQuality) {
				bestMatch = range;
				maxQuality = quality;
			}
		}
		
		return bestMatch;
	}
}
