package migl.lisp;

import java.util.LinkedList;
import java.util.Queue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import migl.util.ConsList;
import migl.util.ConsListFactory;

/**
 * Analyseur Lisp
 * @author Arthur Brandao
 */
public class LispParser {
	
	/**
	 * Regex d'un element lisp
	 */
	private static final String REGEX_LISP_ELEMENT = "[ |\t]*[A-Za-z0-9\\.\\+\\-\\/\\*<>=#]+[ |\t]*";
	
	/**
	 * Regex d'une liste d'elements lisp
	 */
	private static final String REGEX_LISP_LIST = "\\(([ |\t]*[A-Za-z0-9\\.\\+\\-\\/\\*<>=#!]+[ |\t]*)*\\)";
	
	/**
	 * L'expression à analyser
	 */
	private String expr;
	
	/**
	 * L'expression exploser pour l'analyse les données
	 */
	private Queue<String> explode = new LinkedList<>();
	
	/**
	 * Création d'un analyseur lisp
	 */
	public LispParser() {
		this.setExpr("");
	}
	
	/**
	 * Création d'un analyseur lisp
	 * @param lispExpr
	 */
	public LispParser(String lispExpr) {
		this.setExpr(lispExpr);
	}
	
	/**
	 * Change l'expression lisp à analyser
	 * @param lispExpr
	 */
	public final void setExpr(String lispExpr) {
		if(lispExpr == null) {
			throw new IllegalArgumentException("Expression is null");
		}
		this.expr = lispExpr.trim();
	}
	
	/* --- Verification de l'expression --- */
	
	/**
	 * Verifie que le format d'un String correspond bien à une expression Lisp
	 * 
	 * @param expr Le String à examiner
	 * @return
	 */
	public boolean verify() {
		//Pas vide
		if(this.expr.length() == 0) {
			return false;
		}
		//Liste ou element
		if(this.expr.charAt(0) == '(') {
			return this.verifyList();
		}
		return this.verifyElement();
	}
	
	/**
	 * Verifie que le format d'un String correspond bien à un element Lisp
	 * 
	 * @param expr Le String à examiner
	 * @return
	 */
	private boolean verifyElement() {
		Pattern p = Pattern.compile(REGEX_LISP_ELEMENT);
		Matcher m = p.matcher(this.expr);
		return m.find();
	}
	
	/**
	 * Verifie que le format d'un String correspond bien à une liste Lisp
	 * 
	 * @param lispExpr Le String à examiner
	 * @return
	 */
	private boolean verifyList() {
		return this.verifyList(this.expr);
	}
	
	/**
	 * Verifie que le format d'un String correspond bien à une liste Lisp
	 * 
	 * @param lispExpr Le String à examiner
	 * @return
	 */
	private boolean verifyList(String lispExpr) {
		Pattern p = Pattern.compile(REGEX_LISP_LIST);
		Matcher m = p.matcher(lispExpr);
		//Si pas de correspondance
		if(!m.find()) {
			return false;
		} 
		//Si toute la chaine
		else if(m.end() - m.start() == lispExpr.length()) {
			return true;
		} 
		//Si commence au debut mais finit avant la fin il y a alors des elements hors de la liste
		else if(m.start() == 0) {
			return false;
		} 
		//Si il y a une liste dans la liste
		else {
			StringBuilder builder = new StringBuilder();
			builder.append(lispExpr.substring(0, m.start())).append("list").append(lispExpr.substring(m.end()));
			return this.verifyList(builder.toString());
		}
	}
	
	/* --- Parse les elements --- */
	
	/**
	 * Analyse les elements d'une expression lisp
	 * @return
	 * @throws LispError
	 */
	public Object parse() throws LispError {
		//Remplace les nil par ()
		String lispExpr = this.expr.replaceAll(" nil ", " () ")
				.replaceAll("\\(nil ", "(() ")
				.replaceAll(" nil\\)", " ())");
		//Traitement de la chaine pour l'analyer
		this.explode(lispExpr);
		//Analyse le type d'expression
		String val = this.explode.poll();
		if("(".equals(val)){
			return this.parseList();
		} else {
			//Element seul
			if(this.explode.size() > 0) {
				throw new LispError("Invalid Format");
			}
			return LispElement.valueOf(val).getValue();
		}
	}
	
	/**
	 * Separe les différents elements d'une expression lisp pour l'analyse
	 * @param lispExpr
	 */
	private void explode(String lispExpr) {
		String[] tmp = lispExpr.trim().split("[ |\t]");
		for(String str : tmp) {
			char[] charArray = str.toCharArray();
			StringBuilder builder = new StringBuilder();
			for(char c : charArray) {
				if(c == '(') {
					//Si il y a une chaine dans le builder à mettre avant
					if(builder.length() != 0) {
						this.explode.add(builder.toString());
						builder = new StringBuilder();
					}
					this.explode.add("" + c);
				} else if(c == ')') {
					//Si il y a une chaine dans le builder à mettre avant
					if(builder.length() != 0) {
						this.explode.add(builder.toString());
						builder = new StringBuilder();
					}
					this.explode.add("" + c);
				} else {
					builder.append(c);
				}
			}
			//Si il reste une chaine dans le builder à la fin
			if(builder.length() != 0) {
				this.explode.add(builder.toString());
			}
		}
	}
	
	/**
	 * Analyse la liste d'elements
	 * @return
	 */
	private Object parseList() {
		ConsList<Object> list = ConsListFactory.nil();
		String val = this.explode.poll();
		while(!")".equals(val)) {
			if("(".equals(val)) {
				list = list.append(this.parseList());
			} else {
				list = list.append(LispElement.valueOf(val).getValue());
			}
			val = this.explode.poll();
		}
		return list;
	}
	
	/* --- Getter --- */
	
	/**
	 * Retourne l'expression analyser
	 * @return
	 */
	public String getExpression() {
		return this.expr;
	}

}
