package migl.lisp.operator;

import java.util.Map;
import java.util.HashMap;

import migl.lisp.Lisp;
import migl.lisp.LispElement;
import migl.lisp.LispError;
import migl.lisp.LispEval;
import migl.util.ConsList;

public class DefineOperator implements LispOperator {
	
	private final Map<String, ConsList<Object>> define = new HashMap<>();

	@Override
	public LispElement apply(LispEval eval, String operator, ConsList<Object> lisp) throws LispError {
		switch(operator) {
			case "define":
				return this.define(eval, lisp);
			case "set!":
				return this.set(eval, lisp);
			default:
				return this.lambda(eval, operator, lisp);
		}
	}
	
	/**
	 * Operateur lisp define
	 * @param lisp
	 * @return
	 * @throws LispError
	 */
	private LispElement define(LispEval eval, ConsList<Object> lisp) throws LispError {
		if(lisp.size() != 2) {
			throw new LispError(LispError.ERR_NUM_ARG);
		}
		//Recup la clef
		LispElement le = LispElement.generate(lisp.car());
		String key;
		try {
			key = le.toStr();
		} catch(IllegalStateException ex) {
			throw new LispError(le.toString() + LispError.ERR_INVALID, ex);
		}
		//Verification de sa validité
		eval.verifyForbiddenName(key);
		//Ajoute dans la map
		this.define.put(key, lisp.cdr());
		//Evalue pour le retour
		return eval(lisp.cdr());
	}

	/**
	 * Operateur lisp set!
	 * @param lisp
	 * @return
	 * @throws LispError
	 */
	private LispElement set(LispEval eval, ConsList<Object> lisp) throws LispError {
		if(lisp.size() != 2) {
			throw new LispError(LispError.ERR_NUM_ARG);
		}
		//Recup la clef
		String key;
		try {
			key = LispElement.generate(lisp.car()).toStr();
		} catch(IllegalStateException ex) {
			throw new LispError(ex);
		}
		//Verification de sa validité
		eval.verifyForbiddenName(key);
		//Regarde si la valeur est deja presente
		if(!define.containsKey(key)) {
			throw new LispError(key + LispError.ERR_UNKNOW);
		}
		//Ajoute dans la map
		this.define.put(key, lisp.cdr());
		//Evalue pour le retour
		return eval(lisp.cdr());
	}
	
	/**
	 * Evalue une expression lambda
	 * @param operator Le nom de l'expression
	 * @param lisp Les parametres de l'expression
	 * @return La valeur de l'expression
	 * @throws LispError
	 */
	@SuppressWarnings("unchecked")
	private LispElement lambda(LispEval eval, String operator, ConsList<Object> lisp) throws LispError {
		if(!isLambda(operator)) {
			throw new LispError(operator + LispError.ERR_UNKNOW);
		}
		//Recup les infos
		ConsList<Object> lambda;
		ConsList<Object> param;
		try {
			lambda = (ConsList<Object>) this.define.get(operator).car();
			param = (ConsList<Object>) lambda.cdr().car();
			lambda = (ConsList<Object>) lambda.cdr().cdr().car();
			if(param.size() != lisp.size()) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
		} catch (Exception ex) {
			//Lambda mal formée
			throw new LispError(operator + LispError.ERR_INVALID, ex);
		}
		//Remplace les parametres
		String lispExpr = lambda.toString();
		while(!param.isEmpty()) {
			lispExpr = lispExpr.replaceAll(" " + param.car() + " ", " " + lisp.car() + " ")
					.replaceAll("\\(" + param.car() + " ", "\\(" + lisp.car() + " ")
					.replaceAll(" " + param.car() + "\\)", " " + lisp.car() + "\\)");
			param = param.cdr();
			lisp = lisp.cdr();
		}
		//Evalue le resultat
		Lisp l = eval.getInterpreter();
		return LispElement.generate(l.eval(lispExpr));
	}
	
	/**
	 * Indique si un Objet est la representation d'une expression lambda
	 * @param elt L'objet à evaluer
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public boolean isLambda(Object elt) {
		if(!(elt instanceof String)) {
			return false;
		}
		String key = (String) elt;
		ConsList<Object> cl = this.define.get(key);
		if(cl == null) {
			return false;
		}
		if(!(cl.car() instanceof ConsList)) {
			return false;
		}
		cl = (ConsList<Object>) cl.car();
		try {
			return "lambda".equals(LispElement.generate(cl.car()).toStr());
		} catch (IllegalStateException ex) {
			return false;
		}
	}
	
	/**
	 * Evalue un objet
	 * @param elt
	 * @return
	 * @throws LispError
	 */
	@SuppressWarnings("unchecked")
	public LispElement eval(Object elt) throws LispError {
		if(elt instanceof ConsList) {
			return eval((ConsList<Object>) elt);
		}
		//Recup la valeur de l'element
		LispElement lelt = LispElement.generate(elt);
		String key;
		try {
			key = lelt.toStr();
		} catch(IllegalStateException ex) {
			throw new LispError(lelt.toString() + LispError.ERR_UNKNOW, ex);
		}
		//Recup la valeur dans la map
		ConsList<Object> cl = define.get(key);
		if(cl == null) {
			//Si aucune valeur alors erreur
			throw new LispError(key + LispError.ERR_UNKNOW);
		}
		//ToDo verifier si ce n'est pas une lambda expression
		return LispElement.generate(cl.car());
	}
	
	/**
	 * Evalue une liste
	 * @param lisp
	 * @return
	 */
	public LispElement eval(ConsList<Object> lisp) {
		if(lisp.car() instanceof ConsList) {
			String res = lisp.car().toString();
			return LispElement.generate(res.substring(1, res.length() - 1));
		}
		return LispElement.generate(lisp.car());
	}

}
