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

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public abstract class RangeParser<T extends Range> {
	public static class MediaRangeParser extends RangeParser<MediaRange> {
		static final String RE_MEDIA = "(" + RE_TOKEN + ")/(" + RE_TOKEN + ")";

		@Override
		protected String getTagExpr() {
			return RE_MEDIA;
		}

		@Override
		protected MediaRange makeRange(String tag, 
				List<Parameter> media, 
				List<Parameter> accept) throws ParseException {
			
			MediaRange range = new MediaRange(tag, media, accept);
			if (range.getType().equals("*") && ! range.getSubType().equals("*")) {
				throw new ParseException(
					"Cannot use a generic type with a specific subtype", 
					0);
			}
			
			return range;
		}
		
	}
	
	public static class LangRangeParser extends RangeParser<Range> {
		static final String RE_LANG_TAG = "" + RE_ALPHA + "{1,8}";
		static final String RE_LANG = "(" + RE_LANG_TAG + "(-" + RE_LANG_TAG + ")*|\\*)";
		
		@Override
		protected String getTagExpr() {
			return RE_LANG;
		}
		
		@Override
		protected Range makeRange(String tag, 
				List<Parameter> media, 
				List<Parameter> accept) {
			return new Range(tag, media, accept);
		}
	}
	
	static final String RE_ALPHA = "[a-z[A-Z]]";
	static final String RE_CRLF = "(\13\10)";
	static final String RE_LWS = "(" + RE_CRLF + "?[ \7]+)";
	static final String RE_NONCTL = "[^\0-\31]";
	static final String RE_TOKEN = "[^ \127()<>@,;:\\\"/\\[\\]?={}&&" + RE_NONCTL + "]+";
	static final String RE_PARAMETER = "(" + RE_TOKEN + ")=(" + RE_TOKEN + "|\"([^\\\\\"]|\\\\.)*" + "\")";
	static final String RE_PARAMETERS = "(;" + RE_PARAMETER + ")*";
	
	protected abstract String getTagExpr();
	protected abstract T makeRange(String tag, List<Parameter> media,
			List<Parameter> accept) throws ParseException;
	
	String getTagRangeExpr() {
		return getTagExpr() + RE_PARAMETERS;
	}
	
	private void parseParameters(String parameters, List<Parameter> media, List<Parameter> accept) throws ParseException {
		//Parse parameters 
		Pattern parameter = Pattern.compile(RE_PARAMETER + "(;(?=.)|$)");
		Matcher matcher = parameter.matcher(parameters);
		List<Parameter> paramList = media;
		
		int last_end = 0;
		while (matcher.find()) {
			String argument = (matcher.group(1));
			String value = (matcher.group(2));
			
			if (argument.equals("q")) {
				paramList = accept;
			}
			
			paramList.add(new Parameter(argument, value));
			
			if (matcher.start() != last_end) {
				throw new ParseException("Could not parse parameters string", last_end);
			}
			last_end = matcher.end();
		}
		
		//If there is a gap between two matches, something was wrong in the query.
		if (last_end != parameters.length()) {
			throw new ParseException("Could not parse parameters string", last_end);
		}
		
	}
	
	public T parse(String rangeString) throws ParseException {
		Pattern typeDeclaration = Pattern.compile("^(" + getTagExpr() + ")(;(?=.)|$)");
		Matcher typeMatcher = typeDeclaration.matcher(rangeString);
		
		String tag = null;
		List<Parameter> media = new ArrayList<Parameter>();
		List<Parameter> accept = new ArrayList<Parameter>();
		
		if (typeMatcher.find()) {
			tag = typeMatcher.group(1);
			
			if (typeMatcher.group(typeMatcher.groupCount()).equals(";")) {
				// Remove type declaration for the rest of the parsing
				String parameters = rangeString.substring(typeMatcher.end());
				parseParameters(parameters, media, accept);
			}
		}
		else {
			throw new ParseException("Error in type declaration", 0);
		}
		
		return makeRange(tag, media, accept);
	}
	
	public List<T> parseMediaQuery(String mediaQuery) 
		throws ParseException {
		
		List<T> result = new ArrayList<T>();
		final String RE_SEPARATOR   = "(" + RE_LWS + ")*,(" + RE_LWS + ")*";
		Pattern whitespaces = Pattern.compile("^" + RE_LWS + "*"),
				mediaRange 	= Pattern.compile(
						"^(" + this.getTagRangeExpr() + ")((" + RE_SEPARATOR + ")+|$)");
		
		// Trim heading whitespaces
		Matcher wsMatcher =  whitespaces.matcher(mediaQuery);
		mediaQuery = wsMatcher.replaceFirst("");
		
		while (mediaQuery.length() != 0) {
			// Try to find a media query
			Matcher matcher = mediaRange.matcher(mediaQuery);
			
			// Allow null values (i.e. the media range can be absent)
			if (matcher.find()) {
				// If present, parse the media range, and add it to the result
				T range = parse(matcher.group(1));
				result.add(range);
				
				mediaQuery = mediaQuery.substring(matcher.end());
			}
			else {
				throw new ParseException(mediaQuery, 0);
			}
		}
		return result;
	}
}
