package migl.lisp;

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

import migl.util.ConsList;

import java.util.HashMap;

/**
 * Class pour gérer les élément des expressions Lisp
 * 
 * @author Arthur Brandao
 */
public class LispElement {
	
	/**
	 * Garde en cache les elements deja créer pour eviter de recreer plusieurs fois les meme elements
	 */
	private static Map<Object, LispElement> cache = new HashMap<>();

	/**
	 * La valeur de l'element
	 */
	public final Object value;

	/**
	 * Constructeur privée pour construire un element à partir de valueOf
	 * 
	 * @param val
	 */
	private LispElement(Object val) {
		this.value = val;
		cache.put(val, this);
	}
	
	/**
	 * Retourne la valeur d'un element sous forme d'entier
	 * 
	 * @throws IllegalStateException Si l'element ne peut pas être converti en BigInteger
	 * @return
	 */
	public BigInteger toInt() {
		if(this.value.getClass() == BigInteger.class) {
			return (BigInteger) this.value;
		}
		throw new IllegalStateException("Not an integer");
	}
	
	/**
	 * Retourne la valeur d'un element sous forme de nombre
	 * 
	 * Si l'element ne peut pas être converti en Double
	 * @return
	 */
	public double toNumber() {
		if(this.value.getClass() == BigInteger.class) {
			BigInteger bi = (BigInteger) this.value;
			return bi.doubleValue();
		} else if(this.value.getClass() == Double.class) {
			return (Double) this.value;
		}
		throw new IllegalStateException("Not a number");
	}
	
	/**
	 * Retourne la valeur d'un element sous forme de boolean
	 * 
	 * Si l'element ne peut pas être converti en boolean
	 * @return
	 */
	public boolean toBoolean() {
		if(this.value.getClass() == LispBoolean.class) {
			LispBoolean lb = (LispBoolean) this.value;
			return lb.value();
		}
		throw new IllegalStateException("Not a Boolean");
	}
	
	/**
	 * Retourne la valeur d'un element sous forme de string
	 * 
	 * Si l'element ne peut pas être converti en String
	 * @return
	 */
	public String toStr() {
		if(this.value.getClass() == String.class) {
			return (String) this.value;
		}
		throw new IllegalStateException("Not a String");
	}

	@Override
	public int hashCode() {
		return this.value.hashCode();
	}

	@Override
	public boolean equals(Object obj) {
		if(obj == null) {
			return false;
		} else if(this == obj) {
			return true;
		} else if(obj instanceof LispElement) {
			LispElement other = (LispElement) obj;
			return this.value.equals(other.value);
		}
		return false;
	}

	@Override
	public String toString() {
		return this.value.toString();
	}

	/**
	 * Parse un element
	 * 
	 * @param elt L'element à parser
	 * @return Un LispElement contenant la valeur extraite
	 */
	public static LispElement valueOf(String elt) {
		try {
			return generate(new BigInteger(elt));
		} catch(NumberFormatException ex) {
			//Rien
		}
		try {
			return generate(Double.valueOf(elt));
		} catch(NumberFormatException ex) {
			//Rien
		}
		try {
			return generate(LispBoolean.valueOf(elt));
		} catch(IllegalArgumentException ex) {
			//Rien
		}
		return generate(elt);
	}
	
	/**
	 * Genere un LispElement correspondant à l'objet
	 * 
	 * @param elt
	 * @return
	 */
	public static LispElement generate(Object elt) {
		if(elt.getClass() == Integer.class) {
			elt = BigInteger.valueOf((Integer) elt);
		}
		else if(elt.getClass() == Boolean.class) {
			elt = LispBoolean.valueOf((boolean) elt);
		}
		else if(elt.getClass() != BigInteger.class && elt.getClass() != Double.class && elt.getClass() != LispBoolean.class && elt.getClass() != String.class) {
			throw new IllegalArgumentException("Object class is not a Lisp element");
		}
		if(cache.containsKey(elt)) {
			return cache.get(elt);
		}
		return new LispElement(elt);
	}
	
	/**
	 * 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 static LispElement getElement(Object elt) throws LispError {
		if(elt instanceof ConsList) {
			return LispImpl.evaluateList((ConsList<Object>) elt);
		}
		return generate(elt);
	}

}
