package migl.lisp;

import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

import migl.lisp.operator.ComparatorOperator;
import migl.lisp.operator.ConsOperator;
import migl.lisp.operator.DefineOperator;
import migl.lisp.operator.LispOperator;
import migl.lisp.operator.MathOperator;
import migl.lisp.operator.MinMaxOperator;
import migl.util.ConsList;
import migl.util.ConsListFactory;

/**
 * Evaluateur lisp
 * @author Arthur Brandao
 */
public class LispEval {

	/**
	 * Les operateurs gérés par l'interpréteur
	 */
	private Map<String, LispOperator> operators = new HashMap<>();
	
	/**
	 * L'interpreteur utilisant cet evaluateur pour les appels récursifs
	 */
	private final Lisp interpreter;
	
	/**
	 * L'instance de gestion des variables et des fonctions
	 */
	private final DefineOperator define = new DefineOperator();

	/**
	 * Un element lisp
	 */
	private Object lispElt = null;

	/**
	 * Une liste d'elements lisp
	 */
	private ConsList<Object> lispList = null;

	/**
	 * Creation d'un evaluateur lisp
	 */
	public LispEval(Lisp interpreter) {
		this(interpreter, null);
	}

	/**
	 * Creation d'un evaluateur lisp
	 * @param lisp Un element lisp (Object) ou une liste de lisp (ConsList<Object>)
	 */
	public LispEval(Lisp interpreter, Object lisp) {
		this.interpreter = interpreter;
		if(lisp != null) {
			this.setLisp(lisp);
		}
		this.setUpOperators();
	}

	/**
	 * Change l'objet lisp à evaluer
	 * @param lisp Un element lisp (Object) ou une liste de lisp (ConsList<Object>)
	 */
	@SuppressWarnings("unchecked")
	public final void setLisp(Object lisp) {
		if(lisp == null) {
			throw new IllegalArgumentException("Expression is null");
		} else if(lisp instanceof ConsList) {
			this.lispElt = null;
			this.lispList = (ConsList<Object>) lisp;
		} else {
			this.lispElt = lisp;
			this.lispList = null;
		}
	}

	/**
	 * Définit les opérateurs reconnus par l'évaluateur
	 */
	private final void setUpOperators() {
		operators.put("define", this.define);
		operators.put("set!", this.define);
		operators.put("lambda", this.define);
		ConsOperator cons = new ConsOperator();
		operators.put("cons", cons);
		operators.put("car", cons);
		operators.put("cdr", cons);
		ComparatorOperator comp = new ComparatorOperator();
		operators.put(">", comp);
		operators.put(">=", comp);
		operators.put("<", comp);
		operators.put("<=", comp);
		operators.put("=", comp);
		MinMaxOperator minMax = new MinMaxOperator();
		operators.put("min", minMax);
		operators.put("max", minMax);
		MathOperator math = new MathOperator();
		operators.put("cbrt", math);
		operators.put("ceil", math);
		operators.put("floor", math);
		operators.put("log10", math);
		operators.put("cos", math);
		operators.put("rint", math);
		operators.put("round", math);
		operators.put("signum", math);
		operators.put("quote", (eval, op, lisp) -> {
			if(lisp.size() != 1) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			return LispElement.generate(lisp.car().toString());
		});
		operators.put("if", (eval, op, lisp) -> {
			if(lisp.size() != 3) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			if(this.getElement(lisp.car()).toBoolean()) {
				return eval.getElement(lisp.cdr().car());
			} else {
				return eval.getElement(lisp.cdr().cdr().car());
			}
		});
		operators.put("not", (eval, op, lisp) -> {
			if(lisp.size() != 1) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			boolean result = !eval.getElement(lisp.car()).toBoolean();
			return LispElement.generate(result);
		});
		operators.put("and", (eval, op, lisp) -> {
			boolean result = true;
			while(!lisp.isEmpty()) {
				result = result && eval.getElement(lisp.car()).toBoolean();
				lisp = lisp.cdr();
			}
			return LispElement.generate(result);
		});
		operators.put("or", (eval, op, lisp) -> {
			boolean result = false;
			while(!lisp.isEmpty()) {
				result = result || eval.getElement(lisp.car()).toBoolean();
				lisp = lisp.cdr();
			}
			return LispElement.generate(result);
		});
		operators.put("+", (eval, op, lisp) -> {
			BigInteger resultInt = new BigInteger("0");
			while(!lisp.isEmpty()){
				LispElement eltInt = eval.getElement(lisp.car());
				if(eltInt.getValue().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 = eval.getElement(lisp.car());
				result += elt.toNumber();
				lisp = lisp.cdr();
			}
			return LispElement.generate(result);
		});
		operators.put("*", (eval, op, lisp) -> {
			BigInteger resultInt = new BigInteger("1");
			while(!lisp.isEmpty()){
				LispElement eltInt = eval.getElement(lisp.car());
				if(eltInt.getValue().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 = eval.getElement(lisp.car());
				result *= elt.toNumber();
				lisp = lisp.cdr();
			}
			return LispElement.generate(result);
		});
		operators.put("-", (eval, op, lisp) -> {
			switch(lisp.size()) {				
			case 1:
				LispElement elt = eval.getElement(lisp.car());
				if(elt.isInt()) {
					return LispElement.generate(elt.toInt().multiply(new BigInteger("-1")));
				}
				return LispElement.generate(elt.toNumber() * -1); //Pb pitest qui remplace * par / or *-1 == /-1
			case 2:
				LispElement elt1 = eval.getElement(lisp.car());
				LispElement elt2 = eval.getElement(lisp.cdr().car());
				if(elt1.isInt() && elt2.isInt()) {
					return LispElement.generate(elt1.toInt().subtract(elt2.toInt()));
				}
				return LispElement.generate(elt1.toNumber() - elt2.toNumber());
			default:
				throw new LispError(LispError.ERR_NUM_ARG);
			}
		});
		operators.put("/", (eval, op, lisp) -> {
			if(lisp.size() != 2) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			LispElement elt1 = eval.getElement(lisp.car());
			LispElement elt2 = eval.getElement(lisp.cdr().car());
			if(elt2.toNumber() == 0) {
				throw new LispError("Division by zero");
			}
			if(elt1.isInt() && elt2.isInt()) {
				return LispElement.generate(elt1.toInt().divide(elt2.toInt()));
			}
			return LispElement.generate(elt1.toNumber() / elt2.toNumber());
		});
		operators.put("abs", (eval, op, lisp) -> {
			if(lisp.size() != 1) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			LispElement le = eval.getElement(lisp.car());
			if(le.isInt()) {
				return LispElement.generate(le.toInt().abs());
			}
			return LispElement.generate(Math.abs(le.toNumber()));
		});
		operators.put("pow", (eval, op, lisp) -> {
			if(lisp.size() != 2) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			return LispElement.generate(Math.pow(eval.getElement(lisp.car()).toNumber(), eval.getElement(lisp.cdr().car()).toNumber()));
		});
		operators.put("list", (eval, op, lisp) -> {
			LispList list = LispList.nil();
			while(!lisp.isEmpty()) {
				list.append(eval.getElement(lisp.car()));
				lisp = lisp.cdr();
			}
			return LispElement.generate(list);
		});
		operators.put("map", (eval, op, lisp) -> {
			if(lisp.size() != 2) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
			//Regarde si le parametre est une fonction lambda
			if(!eval.getDefine().isLambda(lisp.car())) {
				throw new LispError(lisp.car() + LispError.ERR_INVALID);
			}
			//Regarde si le second parametre est une expression lisp à evaluer
			if(!(lisp.cdr().car() instanceof ConsList)) {
				throw new LispError(lisp.cdr().car() + LispError.ERR_INVALID);
			}
			//Evalue l'expression la valeurs des parametres
			@SuppressWarnings("unchecked")
			LispElement res = eval.evaluateList((ConsList<Object>) lisp.cdr().car());
			if(!res.isList()) {
				return LispElement.generate(LispList.nil());
			}
			//Pour chaque element de la liste evalue la fonction
			LispList result = LispList.nil();
			LispList list = res.toList();
			for(int i = 0; i < list.size(); i++) {
				ConsList<Object> cl = ConsListFactory.asList(lisp.car(), list.get(i));
				result.append(eval.evaluateList(cl));
			}
			return LispElement.generate(result);
		});
	}

	/**
	 * Evalue l'element lisp
	 * @return L'element retourner par l'évaluation
	 * @throws LispError
	 */
	public LispElement evaluate() throws LispError {
		if(this.lispElt != null) {
			return this.evalElt();
		} else if(this.lispList != null) {
			return this.evaluateList();
		}
		return null;
	}

	/**
	 * Evalue un element
	 * @return L'element retourner par l'évaluation
	 * @throws LispError
	 */
	public LispElement evalElt() throws LispError {
		try {
			return this.getElement(this.lispElt);
		} catch (IllegalArgumentException ex) {
			throw new LispError(ex.getMessage(), ex);
		}
	}

	/**
	 * Evalue une liste d'elements lisp
	 * @return L'element retourner par l'évaluation
	 * @throws LispError
	 */
	public LispElement evaluateList() throws LispError {
		return this.evaluateList(this.lispList);
	}

	/**
	 * Evalue un liste d'élément lisp parser dans un ConsList
	 * 
	 * @param lisp La liste parser {@link #parse(String)}
	 * @return Valeur évaluer
	 * @throws LispError
	 */
	@SuppressWarnings("unchecked")
	public LispElement evaluateList(ConsList<Object> lisp) throws LispError {
		//Expression vide
		if(lisp.isEmpty()) {
			return LispElement.generate("()");
		}
		//LCPF
		if(lisp.car() instanceof ConsList) {
			LispElement elt = this.evaluateList((ConsList<Object>) lisp.car());
			ConsList<Object> cl = ConsListFactory.asList(elt.toStr(), lisp.cdr().car());
			return this.evaluateList(cl);
		}
		//Recherche l'operateur
		String operator = LispElement.generate(lisp.car()).toStr();
		LispOperator op = this.operators.get(operator);
		if(op == null) {
			//On suppose que l'operateur est une expression lambda
			return this.operators.get("lambda").apply(this, operator, lisp.cdr());
		}
		try {
			return op.apply(this, operator, lisp.cdr());
		} catch (IllegalStateException ex) {
			throw new LispError(ex.getMessage(), ex);
		} catch (IllegalArgumentException ex) {
			throw new LispError("List Lisp malformed: " + ex.getMessage(), ex);
		}
	}

	/**
	 * Verifie que le nom pour une variable n'est pas interdit
	 * 
	 * @param name Nom pour la variable
	 * @throws LispError Si le nom n'est pas valide
	 */
	public void verifyForbiddenName(String name) throws LispError {
		//Verifie que ce n'est pas une valeur (double, bool, ...)
		try {
			LispElement.valueOf(name).toStr();
		} catch (IllegalStateException ex) {
			throw new LispError(name + LispError.ERR_INVALID, ex);
		}
		//Verifie que ce n'est pas le nom d'un operateur
		if(this.operators.containsKey(name)) {
			throw new LispError(name + LispError.ERR_INVALID);
		}
	}
	
	/**
	 * Récupère un élément dans la liste lisp
	 * Si l'element est une liste elle seras alors analyser
	 * 
	 * @param elt L'element à récupérer
	 * @return L'element ou le resultat de l'analyse si l'element est une liste
	 * @throws LispError
	 */
	@SuppressWarnings("unchecked")
	public LispElement getElement(Object elt) throws LispError {
		if(elt instanceof ConsList) {
			return this.evaluateList((ConsList<Object>) elt);
		} else if(elt instanceof String) {
			//Si c'est un string qui n'est pas un nom reservé
			String str = (String) elt;
			boolean skip = false;
			try {
				this.verifyForbiddenName(str);
			} catch (LispError ex) {
				skip = true;
			}
			if(!skip) return this.define.eval(elt);
		}
		return LispElement.generate(elt);
	}
	
	/**
	 * Retourne l'interpreteur utilisant cet évaluateur
	 * @return
	 */
	public Lisp getInterpreter() {
		return this.interpreter;
	}
	
	/**
	 * Retourne l'instance de gestion des variables et des fonctions
	 * @return
	 */
	public DefineOperator getDefine() {
		return this.define;
	}
	
}
