package migl.lisp;

import java.math.BigInteger;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import migl.lisp.operator.ComparatorOperator;
import migl.lisp.operator.ConsOperator;
import migl.util.ConsList;
import migl.util.ConsListFactory;

public class LispImpl implements Lisp {
	
	
	
	/**
	 * Les operateurs gérés par l'interpréteur
	 */
	private static Map<String, LispOperator> operators = new HashMap<>();
	static {
		//Définition des opérateurs
		operators.put("cons", new ConsOperator());
		operators.put(">", new ComparatorOperator());
		operators.put(">=", new ComparatorOperator());
		operators.put("<", new ComparatorOperator());
		operators.put("<=", new ComparatorOperator());
		operators.put("=", new ComparatorOperator());
		operators.put("quote", (op, lisp) -> {
			if(lisp.size() != 1) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			return LispElement.generate(lisp.car().toString());
		});
		operators.put("if", (op, lisp) -> {
			if(lisp.size() != 3) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			if(LispElement.getElement(lisp.car()).toBoolean()) {
				return LispElement.getElement(lisp.cdr().car());
			} else {
				return LispElement.getElement(lisp.cdr().cdr().car());
			}
		});
		operators.put("not", (op, lisp) -> {
			if(lisp.size() != 1) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			boolean result = !LispElement.getElement(lisp.car()).toBoolean();
			return LispElement.generate(result);
		});
		operators.put("and", (op, lisp) -> {
			boolean result = true;
			while(!lisp.isEmpty()) {
				result = result && LispElement.getElement(lisp.car()).toBoolean();
				lisp = lisp.cdr();
			}
			return LispElement.generate(result);
		});
		operators.put("or", (op, lisp) -> {
			boolean result = false;
			while(!lisp.isEmpty()) {
				result = result || LispElement.getElement(lisp.car()).toBoolean();
				lisp = lisp.cdr();
			}
			return LispElement.generate(result);
		});
		operators.put("+", (op, lisp) -> {
			BigInteger resultInt = new BigInteger("0");
            while(!lisp.isEmpty()){
                LispElement eltInt = LispElement.getElement(lisp.car());
                if(eltInt.value.getClass() != BigInteger.class) break;
                resultInt = resultInt.add(eltInt.toInt());
                lisp = lisp.cdr();
            }
            //Si on finit la liste avec que des entier on retourne
            if(lisp.isEmpty()) return LispElement.generate(resultInt);
            //Sinon on continue en passant en double
            double result = resultInt.doubleValue();
            while(!lisp.isEmpty()) {
                LispElement elt = LispElement.getElement(lisp.car());
                result += elt.toNumber();
                lisp = lisp.cdr();
            }
            return LispElement.generate(result);
		});
		operators.put("*", (op, lisp) -> {
			BigInteger resultInt = new BigInteger("1");
            while(!lisp.isEmpty()){
                LispElement eltInt = LispElement.getElement(lisp.car());
                if(eltInt.value.getClass() != BigInteger.class) break;
                resultInt = resultInt.multiply(eltInt.toInt());
                lisp = lisp.cdr();
            }
            //Si on finit la liste avec que des entier on retourne
            if(lisp.isEmpty()) return LispElement.generate(resultInt);
            //Sinon on continue en passant en double
            double result = resultInt.doubleValue();
            while(!lisp.isEmpty()) {
                LispElement elt = LispElement.getElement(lisp.car());
                result *= elt.toNumber();
                lisp = lisp.cdr();
            }
            return LispElement.generate(result);
		});
		operators.put("-", (op, lisp) -> {
			switch(lisp.size()) {				
				case 1:
					LispElement elt = LispElement.getElement(lisp.car());
					if(elt.value.getClass() == Double.class) {
						//return LispElement.generate(elt.toNumber() * -1); //Pb pitest qui remplace * par / or *-1 == /-1
						double value = elt.toNumber();
						return LispElement.generate(value - (value * 2));
					}
					return LispElement.generate(elt.toInt().multiply(new BigInteger("-1")));
				case 2:
					LispElement elt1 = LispElement.getElement(lisp.car());
					LispElement elt2 = LispElement.getElement(lisp.cdr().car());
					if(elt1.value.getClass() == Double.class || elt2.value.getClass() == Double.class) {
						return LispElement.generate(elt1.toNumber() - elt2.toNumber());
					}
					return LispElement.generate(elt1.toInt().subtract(elt2.toInt()));
				default:
					throw new LispError(LispError.ERR_NUM_ARG);
			}
		});
		operators.put("/", (op, lisp) -> {
			if(lisp.size() != 2) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			LispElement elt1 = LispElement.getElement(lisp.car());
			LispElement elt2 = LispElement.getElement(lisp.cdr().car());
			if(elt2.toNumber() == 0) {
				throw new LispError("Division by zero");
			}
			if(elt1.value.getClass() == Double.class || elt2.value.getClass() == Double.class) {
				return LispElement.generate(elt1.toNumber() / elt2.toNumber());
			}
			return LispElement.generate(elt1.toInt().divide(elt2.toInt()));
		});
	}
	
	@Override
	public Object parse(String expr) throws LispError {
		//Analyse l'expression
		if(!this.verify(expr)) {
			throw new LispError("Invalid Format");
		}
		//Remplace les nil par ()
		expr = expr.replaceAll(" nil", " ()");
		//Explose l'expression
		Queue<String> explode = this.explode(expr);
		//Analyse le type d'expression
		String val = explode.poll();
		if("(".equals(val)){
			return this.parseList(explode);
		} else {
			//Element seul
			if(explode.size() > 0) {
				throw new LispError("Invalid Format");
			}
			return LispElement.valueOf(val).value;
		}
	}

	@Override
	@SuppressWarnings("unchecked")
	public Object evaluate(Object lisp) throws LispError {
		if(lisp instanceof ConsList) {
			return evaluateList((ConsList<Object>) lisp);
		}
		//Si element seul on évalue directement sa valeur
		try {
			return LispElement.generate(lisp);
		} catch (IllegalArgumentException ex) {
			throw new LispError(ex.getMessage(), ex);
		}
		
	}
	
	/* --- Verification --- */
	
	/**
	 * Verifie que le format d'un String correspond bien à une expression Lisp
	 * 
	 * @param expr Le String à examiner
	 * @return
	 */
	private boolean verify(String expr) {
		expr = expr.trim();
		//Pas vide
		if(expr.length() == 0) {
			return false;
		}
		//Liste ou element
		if(expr.charAt(0) == '(') {
			return this.verifyList(expr);
		}
		return this.verifyElement(expr);
	}
	
	/**
	 * Verifie que le format d'un String correspond bien à une liste Lisp
	 * 
	 * @param expr Le String à examiner
	 * @return
	 */
	private boolean verifyList(String expr) {
		Pattern p = Pattern.compile("\\(([ |\t]*[A-Za-z0-9\\.\\+\\-\\/\\*<>=#]+[ |\t]*)*\\)");
		Matcher m = p.matcher(expr);
		//Si pas de correspondance
		if(!m.find()) {
			return false;
		} 
		//Si toute la chaine
		else if(m.end() - m.start() == expr.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(expr.substring(0, m.start())).append("list").append(expr.substring(m.end()));
			return this.verifyList(builder.toString());
		}
	}
	
	/**
	 * Verifie que le format d'un String correspond bien à un element Lisp
	 * 
	 * @param expr Le String à examiner
	 * @return
	 */
	private boolean verifyElement(String expr) {
		Pattern p = Pattern.compile("[ |\t]*[A-Za-z0-9\\.\\+\\-\\/\\*<>=#]+[ |\t]*");
		Matcher m = p.matcher(expr);
		return m.find();
	}
	
	/* --- Parser --- */
	
	/**
	 * Découpe un string pour l'analyse Lisp
	 * 
	 * @param expr L'expression à analyser
	 * @return L'expression découpée
	 */
	private Queue<String> explode(String expr) {
		String[] tmp = expr.trim().split("[ |\t]");
		Queue<String> result = new LinkedList<>();
		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) {
						result.add(builder.toString());
						builder = new StringBuilder();
					}
					result.add("" + c);
				} else if(c == ')') {
					//Si il y a une chaine dans le builder à mettre avant
					if(builder.length() != 0) {
						result.add(builder.toString());
						builder = new StringBuilder();
					}
					result.add("" + c);
				} else {
					builder.append(c);
				}
			}
			//Si il reste une chaine dans le builder à la fin
			if(builder.length() != 0) {
				result.add(builder.toString());
			}
		}
		return result;
	}
	
	/**
	 * Parse une file d'element en Lisp
	 * 
	 * @param queue La file avec les valeurs récupérées {@link #explode(String)}
	 * @return Liste d'element
	 * @throws LispError Format invalide
	 */
	private Object parseList(Queue<String> queue) throws LispError {
		ConsList<Object> list = ConsListFactory.nil();
		String val = queue.poll();
		while(!")".equals(val)) {
			if("(".equals(val)) {
				list = list.append(this.parseList(queue));
			} else {
				list = list.append(LispElement.valueOf(val).value);
			}
			val = queue.poll();
		}
		return list;
	}
	
	/* --- Evaluation --- */
	
	/**
	 * Evalue un liste d'élément lisp parser dans un ConsList
	 * 
	 * @param lisp La liste parser {@link #parse(String)}
	 * @return Valeur évaluer
	 * @throws LispError
	 */
	public static LispElement evaluateList(ConsList<Object> lisp) throws LispError {
		if(lisp.isEmpty()) {
			return LispElement.generate("()");
		}
		String operator = LispElement.generate(lisp.car()).toStr();
		LispOperator op = operators.get(operator);
		if(op == null) {
			throw new LispError(new UnsupportedOperationException("Unknow expression"));
		}
		try {
			return op.apply(operator, lisp.cdr());
		} catch (IllegalStateException ex) {
			throw new LispError(ex.getMessage(), ex);
		} catch (IllegalArgumentException ex) {
			throw new LispError("List Lisp malformed: " + ex.getMessage(), ex);
		}
	}

}
