package migl.lisp;

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

import migl.util.Cons;

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
	 */
	private 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 de l'element lisp
	 * @return
	 */
	public Object getValue() {
		return this.value;
	}
	
	/**
	 * 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.isInt()) {
			return (BigInteger) this.value;
		}
		throw new IllegalStateException("Not an integer");
	}
	
	/**
	 * Retourne la valeur d'un element sous forme de nombre
	 * 
	 * @throws IllegalStateException 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
	 * 
	 * @throws IllegalStateException Si l'element ne peut pas être converti en boolean
	 * @return
	 */
	public boolean toBoolean() {
		if(this.isBoolean()) {
			LispBoolean lb = (LispBoolean) this.value;
			return lb.value();
		}
		throw new IllegalStateException("Not a Boolean");
	}
	
	/**
	 * Retourne la valeur d'un element sous forme de string
	 * 
	 * @throws IllegalStateException Si l'element ne peut pas être converti en String
	 * @return
	 */
	public String toStr() {
		if(this.isStr()) {
			return (String) this.value;
		}
		throw new IllegalStateException("Not a String");
	}
	
	/**
	 * Retourne la valeur d'un element sous forme d'une liste
	 * 
	 * @throws IllegalStateException
	 * @return
	 */
	public LispList toList() {
		if(this.isList()) {
			return (LispList) this.value;
		}
		throw new IllegalStateException("Not a List");
	}
	
	/**
	 * Retourne la valeur d'un element sous forme d'une cons
	 * 
	 * @throws IllegalStateException
	 * @return
	 */
	public Cons<?, ?> toCons() {
		if(this.isCons()) {
			return (Cons<?, ?>) this.value;
		}
		throw new IllegalStateException("Not a Cons");
	}
	
	/**
	 * Indique si l'element est un entier
	 * @return
	 */
	public boolean isInt() {
		return this.value.getClass() == BigInteger.class;
	}
	
	/**
	 * Indique si l'element est nombre (BigInteger ou Double)
	 * @return
	 */
	public boolean isNumber() {
		return this.isInt() || (this.value.getClass() == Double.class);
	}
	
	/**
	 * Indique si l'element est boolean
	 * @return
	 */
	public boolean isBoolean() {
		return this.value.getClass() == LispBoolean.class;
	}
	
	/**
	 * Indique si l'element est un String
	 * @return
	 */
	public boolean isStr() {
		return this.value.getClass() == String.class;
	}
	
	/**
	 * Indique si l'element est une liste
	 * @return
	 */
	public boolean isList() {
		return this.value.getClass() == LispList.class;
	}
	
	/**
	 * Indique si l'element est une Cons
	 * @return
	 */
	public boolean isCons() {
		return this.value.getClass() == Cons.class;
	}

	@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();
	}
	
	/**
	 * Indique le nombre actuel d'element en cache
	 * @return
	 */
	public static int getCacheSize() {
		return cache.size();
	}
	
	/**
	 * Supprime le cache
	 */
	public static void clear() {
		cache.clear();
	}

	/**
	 * 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 == null) {
			return new LispElement(null);
		}
		else if(elt.getClass() == LispElement.class) {
			return (LispElement) elt;
		} 
		else 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() == Long.class) {
			elt = BigInteger.valueOf((long) elt);
		}
		else if(
				elt.getClass() != BigInteger.class 
				&& elt.getClass() != Double.class 
				&& elt.getClass() != LispBoolean.class 
				&& elt.getClass() != String.class
				&& elt.getClass() != LispList.class
				&& elt.getClass() != Cons.class
				) {
			throw new IllegalArgumentException("Object class is not a Lisp element");
		}
		if(cache.containsKey(elt)) {
			return cache.get(elt);
		}
		return new LispElement(elt);
	}

}
