package migl.lisp.operator;

import java.util.Map;
import java.util.Random;
import java.util.ArrayList;
import java.util.HashMap;

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

public class DefineOperator implements LispOperator {
	
	/**
	 * Taille des nom aléatoire générés
	 */
	private static final int RANDOM_NAME_LENGTH = 8;

	/**
	 * Tableau des variables et fonctions définit
	 */
	private final Map<String, ConsList<Object>> define = new HashMap<>();
	
	/**
	 * Liste des nom de variables/fonctions temporaires
	 */
	private final ArrayList<String> tmp = new ArrayList<>();

	@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(lisp);
			case "lambda":
				String name = this.generateRandomName();
				ConsList<Object> lambda = ConsListFactory.asList("lambda", lisp.car(), lisp.cdr().car());
				ConsList<Object> cl = ConsListFactory.asList("define", name, lambda);
				eval.evaluateList(cl);
				return LispElement.generate(name);
			default:
				return this.lambda(eval, operator, lisp);
		}
	}
	
	/**
	 * Operateur lisp define
	 * @param lisp
	 * @return
	 * @throws LispError
	 */
	@SuppressWarnings("unchecked")
	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);
		//Si c'est une lambda verification du nombre d'argument
		if(lisp.cdr().car() instanceof ConsList) {
			ConsList<Object> cl = (ConsList<Object>) lisp.cdr().car();
			if(cl.size() != 3) {
				throw new LispError(LispError.ERR_NUM_ARG);
			}
		}
		//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(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);
		}
		//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()) {
			try {
				//Tentative d'evalutation des arguments
				LispElement elt = eval.evaluateList((ConsList<Object>) lisp.car());
				lispExpr = lispExpr.replaceAll(" " + param.car() + " ", " " + elt + " ")
						.replaceAll("\\(" + param.car() + " ", "\\(" + elt + " ")
						.replaceAll(" " + param.car() + "\\)", " " + elt + "\\)");
			} catch (Exception ex) {
				lispExpr = lispExpr.replaceAll(" " + param.car() + " ", " " + lisp.car() + " ")
						.replaceAll("\\(" + param.car() + " ", "\\(" + lisp.car() + " ")
						.replaceAll(" " + param.car() + "\\)", " " + lisp.car() + "\\)");
			}
			param = param.cdr();
			lisp = lisp.cdr();
		}
		//Si c'etait une fonction temporaire
		if(this.tmp.contains(operator)) {
			this.tmp.remove(operator);
			this.define.remove(operator);
		}
		//Evalue le resultat
		Lisp interpreter = eval.getInterpreter();
		return LispElement.generate(interpreter.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());
	}
	
	/**
	 * Generation d'un string aleatoire
	 * @return
	 */
	public String generateRandomName() {
		Random rand = new Random();
		StringBuilder builder = new StringBuilder();
		for(int i = 0; i < RANDOM_NAME_LENGTH; i++) {
			builder.append((char) (rand.nextInt(26) + 97));
		}
		String str = builder.toString();
		this.tmp.add(str);
		return str;
	}

}
