package de.brightbyte.yates;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import de.brightbyte.util.StringUtils;
import de.brightbyte.util.StructuredDataCodec;

/** 
 * YATeS: Yet Another Template System.
 * <p>Syntax:
 * <dl>
 * <dt>{{$foo}}</dt><dd>evaluates property foo from the model, escaped</dd>
 * <dt>{{$foo.bar.xyzzy}}</dt><dd>properties can be accessed recursively</dd>
 * <dt>{{roo}}</dt><dd>applies macro roo from the context, result is used verbatim</dd>
 * <dt>{{roo|a=3, x="y x z", bert=$something}}</dt><dd>parameters controll behavior of the macro.
 * 		Parameters can be integers, floats, booleans, string, null, or references to model variables. 
 * 		</dd>
 * <dt>{{roo|&name="Yates"}}</dt><dd>Parameters starting with & override (shadow) model values</dd>
 * <dt>{{wrap|mode="x"|some more {{stuff}}}}</dt><dd>In addition to parameters, a macro can also take
 *      content, which will be parsed recursively. If the macro is a template, content becomes 
 *      available internally as {{_}} (varbatim) or {{$_}} (escaped). 
 *      </dd>
 * <dt>{{wrap|mode="x" `some more text`}}</dt><dd>Unparsed content can be supplied
 *     using tick-quotes, ` or ´</dd>
 * <dt>{{* bla bla *}}</dt><dd>Comments must have matching start and end markers. Supported are
 * 		{{* *}}, {{% %}}, {{! !}}, {{- -}}, {{~ ~}}, {{# #}} 
 * 		</dd>
 * <dt>{{" bla bla "}}</dt><dd>Text within quoteed block is not parsed, but is used verbatim.
 *      Quoted blocks must have matching start and end markers. Supported are
 * 		{{" "}}, {{' '}}, {{´ ´}}, {{` `}} 
 * 		</dd>
 * <dt>{{|if=$x|some {{stuff}} here}}</dt><dd>Anonymous block can be used to apply parameters
 *      for the given content. Usefull especially for conditional sections.
 *      </dd>
 * <dt>{{|escape=$jsEscape `some text here`}}</dt><dd>Anonymous block can also have 
 * 		quoted (verbatim) content.
 *      </dd>
 * <dt>{{foreach|for=$someList|do some {{stuff}}}}</dt><dd>A foreach macro can be used to
 * 		apply the content for every item in a list (the list may be an Interable or an array).
 * 		Each item becomes the model for one evaulation of the content.
 * 		</dd>
 * <dt>{{$lbrace}}x{{$rbrace}}</dt><dd>lbrace and rbrace can be used to represent { and } in contexts
 * 		where other types of quoting are not desirable.
 *      </dd>
 * </dl>
 * Special values: {{$settings.foo}}, {{$data}}
 * Special parameters: default, escape, if/unless, equals, subject, renderer  
 * </p>
 **/
public class YatesTemplate implements YatesMacro {
	
	protected static final Pattern paramChunker = Pattern.compile(
			"\\s*([\\]\\[{},|=`´]|null|true|false|[FD]?-?\\d+(\\.\\d+([eE]-?\\d+)?)?|[BSIL]?-?\\d+|\"(\\\\.|[^\"\\\\]+)*\"|'([^'\\\\]|\\\\'|\\\\\\\\)'|[&$]?[\\w.:]+)\\s*", Pattern.DOTALL);

	protected static final Pattern paramIdent = Pattern.compile("&?(\\.|\\w+)");

	//private static final String VALUE_PATTERN = "\\$\\w[\\w\\d]*|\\d+|true|false";
	//protected static final Pattern macroPattern = Pattern.compile(
	//		"\\{\\{\\s*([$]?\\w[\\w\\d:.]*)(\\s*\\|\\s*(.*?))?\\s*\\}\\}", Pattern.DOTALL );

	protected static interface Chunk {
		public String eval(YatesParameters parameters) throws YatesException;
	}
	
	protected static class Literal implements Chunk {
		private String text;

		public Literal(String text) {
			super();
			this.text = text;
		}

		public String eval(YatesParameters parameters) {
			return text;
		}
		
		@Override
		public String toString() {
			return text;
		}
		
	}
	
	protected class Macro implements Chunk {
		private final String macro;
		private final Map<String, Object> args;
		private final Object inner;

		public Macro(String macro, Map<String, Object> args, Object inner) {
			if (macro==null) throw new NullPointerException("macro must not be null");
			if (macro.length()==0) throw new NullPointerException("macro name must not be empty");
			
			this.macro = macro;
			this.args = args;
			this.inner = inner;
		}
		
		public String eval(final YatesParameters parameters) throws YatesException {
			Map<String, Object> a = args;

			if (inner!=null) {
				YatesMacro r;
				if (inner instanceof YatesMacro) r = (YatesMacro)inner;
				else if (inner instanceof String) r = new LiteralMacro((String)inner);
				else r = new ChunkRenderer((List<Chunk>)inner);

				if (a==null) a = new HashMap<String, Object>();
				a.put("_", r);
				a.put("&_", r);
			}
			
			return YatesTemplate.this.eval(macro, parameters, a);
		}
		
		@Override
		public String toString() {
			return "{{"+macro+"}}";
		}
	}
	
	protected class ChunkRenderer implements YatesMacro {
		protected List<Chunk> chunks;
		
		public ChunkRenderer(List<Chunk> chunks) {
			this.chunks = chunks;
		}

		public String render(YatesParameters parameters) throws YatesException {
			StringBuilder buffer = new StringBuilder();
			
			for (Chunk chunk: chunks) {
				String s = chunk.eval(parameters);
				buffer.append(s);
			}
			
			return buffer.toString();
		}
	
		@Override
		public String toString() {
			return chunks.toString();
		}
	}
	
	protected List<Chunk> chunks;
	
	public YatesTemplate(String templateText) throws YatesException {
		chunks = new Parser(templateText).parse(false);
	}
	
	protected static final String QUOTE_CHARS = "`´\"'";
	protected static final String COMMENT_CHARS = "-*#~!%"; //NOTE: "-" must be first, so char-class syntax doesn't get confused
	
	protected static final Pattern macroStart = Pattern.compile(
			"\\{\\{\\s*([$]?[\\w.][\\w\\d:.]*)\\s*(\\|\\s*)?|(\\{\\{\\|)|(\\{\\{["+QUOTE_CHARS+COMMENT_CHARS+"])|(\\}\\})", Pattern.DOTALL );
	protected static final Pattern macroFlatEnd = Pattern.compile(
			"["+COMMENT_CHARS+QUOTE_CHARS+"]\\}\\}", Pattern.DOTALL );
	protected static final Pattern macroEnd = Pattern.compile(
			"\\}\\}", Pattern.DOTALL );
	
	protected static class ParameterScanner extends StructuredDataCodec.Scanner {

		public ParameterScanner(Pattern chunker, String s) {
			super(chunker, s);
		}

		public int scanParameters() {
			boolean first = true;
			
			while (true) {
				String chunk = next();
				if (chunk==StructuredDataCodec.EOF) throw new IllegalArgumentException("premature end of input");
				if (chunk.equals("}") || chunk.equals("|") || chunk.equals("`") || chunk.equals("´")) {
					pushback();
					break;
				}
				
				if (first) first = false;
				else {
					if (!chunk.equals(",")) throw new IllegalArgumentException("bad input, ´,´ or ´|´ expected");
					chunk = next();
					if (chunk==StructuredDataCodec.EOF) throw new IllegalArgumentException("premature end of input");
				}

				scanChunk(chunk, false);

				chunk = next();
				if (chunk==StructuredDataCodec.EOF) throw new IllegalArgumentException("premature end of input");
				if (!chunk.equals("=")) throw new IllegalArgumentException("bad input, ´:´ or ´=´ expected");

				chunk = next();
				if (chunk==StructuredDataCodec.EOF) throw new IllegalArgumentException("premature end of input");
				scanChunk(chunk, false);
			}
				
			return pos;
		}
		
	}
	
	protected class Parser {
		protected int index = 0;
		protected String template;
		
		protected Matcher start = null;
		protected Matcher end = null;
		protected Matcher flatEnd = null;
		
		protected ParameterScanner paramScanner;
		
		public Parser(String template) {
			this.template = template;
		}
		
		protected ParameterScanner getParamScanner() {
			if (paramScanner==null) paramScanner = new ParameterScanner(paramChunker, template);
			paramScanner.seek(index);
			return paramScanner;
		}
		
		protected Matcher getStartMatcher() {
			if (start==null) start = macroStart.matcher(template);
			start.region(index, template.length());
			return start;
		}
	
		protected Matcher getEndMatcher() {
			if (end==null) end = macroEnd.matcher(template);
			end.region(index, template.length());
			return end;
		}
	
		protected Matcher getFlatEndMatcher() {
			if (flatEnd==null) flatEnd = macroFlatEnd.matcher(template);
			flatEnd.region(index, template.length());
			return flatEnd;
		}
		
		protected String scanQuotedBlock(char marker) throws YatesException {
			int i = index;
			String s;
			while (true) { //end must match marker
				if (!getFlatEndMatcher().find()) {
					throw new YatesException("unclosed block starting at "+StringUtils.describeLocation(template, i, i)); 
				}
				
				s = flatEnd.group();
				
				index = flatEnd.end();
				if (s.charAt(0)==marker) break;
			}
								
			//quoted, use literal
			s = template.substring(i, flatEnd.start());
			return s;
		}
	
		protected List<Chunk> parse(boolean allowEnd) throws YatesException {
			ArrayList<Chunk> chunks = new ArrayList<Chunk>();
			
			int finish = template.length();
			while (getStartMatcher().find() && index<finish) {
				String txt = template.substring(index, start.start());
				if (txt.length()>0) chunks.add(new Literal(txt));
				
				index = start.end();
				
				String macro;
	
				if (start.group(5)!=null) { //found end marker }}, pop recursive call (or die)
					if (allowEnd) {
						finish = start.start();
						break; 
					}
					else {
						throw new YatesException("unexpected closing parantecies: "+StringUtils.describeLocation(template, start.start(), start.end()));
					}
				}
				else if (start.group(4)!=null) { //found comment or quoted block
					String s = start.group(4);
					char ch = s.charAt(s.length()-1);
					boolean comment = COMMENT_CHARS.indexOf(ch)>=0;
					
					s = scanQuotedBlock(ch);
					if (!comment) chunks.add(new Literal(s));
					
					continue;
				}
				else if (start.group(3)!=null) { //anonymous block
					macro = "block";
				}
				else { //found macro or structure
					macro = start.group(1);
				}
				
				String params = parseParams(); 
				char ch = template.charAt(index); 
				
				Object inner = null;
				
				if (ch=='`' || ch=='´') { //quoted
					index ++;
					String s = scanQuotedBlock(ch);
					inner = new LiteralMacro(s); 
				}
				else if (ch=='|') { //nested - recurse!
					index ++;
					inner = parse(true); //NOTE: recurse! (also eats end delimiter }})
				}
				else { //no inner structure
					if (!getEndMatcher().lookingAt()) {
						throw new YatesException("unclosed macro starting at "+StringUtils.describeLocation(template, start.start(), start.start()));
					}
					
					index = end.end();
				}
				
				StructuredDataCodec codec = new StructuredDataCodec(YatesParameters.referenceLookup, paramChunker, paramIdent);
				
				Map<String, Object> a = null;
				
				if (params!=null && params.length()>0) {
					params = "{"+params+"}";
				}
				
				if (params!=null && params.length()>0) {
					a = (Map<String, Object>)codec.decodeValue(params);
				}
							
				chunks.add(new Macro(macro, a, inner));
			}
			
			if (index<finish) {
				String txt = template.substring(index, finish);
				if (txt.length()>0) chunks.add(new Literal(txt));
			}
			
			return chunks;
		}

		private String parseParams() {
			ParameterScanner scanner = getParamScanner();
			int i = scanner.scanParameters();
			String p = template.substring(index, i);
			index = i;
			return p;
		}
	
	}

	public String render(YatesParameters parameters) throws YatesException {
		StringBuffer buffer = new StringBuffer();
		
		for (Chunk chunk: chunks) {
			String s = chunk.eval(parameters);
			buffer.append(s);
		}
		
		return buffer.toString();
	}

	protected String eval(String macroName, YatesParameters parentParam, Map<String, Object> args) throws YatesException {
		YatesMacro macro;
		Object subject = null;
		String s;
		StringMangler esc;
		YatesParameters parameters = new YatesParameters(parentParam, args);

		//TODO: handle if/unless
		/*if (macroName.equals("$_")) {
	 	    macro = (YatesMacro)parameters.getParameter("__", null); //HACKish
			if (macro==null) throw new YatesException("unknown template: "+macroName);

			subject = parameters.getParameter(".", subject); 
			esc = (StringMangler)parameters.getParameter("escape", parameters.getEscaper());
		}
		else*/ if (macroName.charAt(0)=='$') {
			macroName = macroName.substring(1);
			
			if (macroName.equals("lbrace")) subject = "{"; //XXX: doc!
			else if (macroName.equals("rbrace")) subject = "}"; //XXX: doc!
			else {
				subject = parameters.resolve(macroName);
				subject = parameters.fallbackParameter("default", subject);
			}
			
			macro = (YatesMacro)parameters.getParameter("renderer", parameters.guessRenderer(subject));
			esc = (StringMangler)parameters.getParameter("escape", parameters.getEscaper());
		}
		else {
			/*
			if (macroName.equals("_")) macro = (YatesMacro)parameters.getParameter("__", null); //HACKish
			else*/ 
			macro = parameters.getMacro(macroName);
			
			if (macro==null) 
				throw new YatesException("unknown macro: "+macroName);
			
			//subject = parentParam.getParameter("model", subject);
			esc = (StringMangler)parameters.getParameter("escape", null);
		}

		if (subject!=null) parameters.setSubject(subject);
		
		Object eq = parameters.getParameter("equals", null);
		
		if (parameters.hasParameter("if") && !evalCondition(parameters.getParameter("if", null), eq)) return "";
		if (parameters.hasParameter("unless") && evalCondition(parameters.getParameter("unless", null), eq)) return "";
		
		
		s = macro.render(parameters);
		
		if (esc!=null) s = esc.apply(s);

		return s;
	}

	protected boolean evalCondition(Object v, Object eq) {
		if (eq!=null) { //FIXME: this is not clean- use separate flag!
			if (v==eq || (v!=null && v.equals(eq))) return true;
			return false;
		}
		else {
			if (v==null) return false;
			else if (v instanceof Boolean) return ((Boolean)v).booleanValue();
			else if (v instanceof String) return ((String)v).length()>0;
			else if (v instanceof Number) return ((Number)v).doubleValue() != 0;
			else if (v instanceof Collection) return !((Collection)v).isEmpty();
			else if (v instanceof Map) return !((Map)v).isEmpty();
			else if (v instanceof Object[]) return ((Object[])v).length==0;
			
			return true;
		}
	}

	@Override
	public String toString() {
		return chunks.toString();
	}
}
