
package org.biomoby.shared.data;
import java.math.BigDecimal;
import java.math.BigInteger;

import org.biomoby.registry.meta.Registry;
import org.biomoby.shared.MobyDataType;
import org.biomoby.shared.MobyNamespace;
import org.biomoby.shared.parser.MobyTags;

/**
 * A class representing a MOBY Float primitive.  Note that the 
 * Float notion in MOBY does not define a specific bit precision.
 * All values in this class are stored as Java BigDecimal, which is 
 * arbitary precision to avoid loss of data integrity.
 *
 * Because getObject() will return an immutable BigDecimal, basic arithmetic
 * methods have been included to allow modification of the underlying value.
 */

public class MobyDataFloat extends MobyDataObject{

    private BigDecimal value;
    
    /**
     * Construct the object using a DOM fragment.
     *
     * @throws IllegalArgumentException if the element is not a Float tag
     */
    public MobyDataFloat(org.w3c.dom.Element element) throws IllegalArgumentException{
	this(element, null);
    }

    public MobyDataFloat(org.w3c.dom.Element element, Registry registry) throws IllegalArgumentException{
	this(getName(element), getTextContents(element), registry);
	setId(getId(element));
	addNamespace(getNamespace(element, registry));
    }

    /**
     * Constructor to use if the incoming value is a number object such as 
     * Float, Double, Integer, BigDecimal, etc.
     */
    public MobyDataFloat(String articleName, Number n){
	this(articleName, n, null);
    }

    public MobyDataFloat(String articleName, Number n, Registry r){
	super(articleName, r);
	setDataType(MobyDataType.getDataType(MobyTags.MOBYFLOAT, r));
	if(n instanceof BigDecimal){
	    value = (BigDecimal) n;
	}
	else if(n instanceof BigInteger){
	    value = new BigDecimal(((BigInteger) n).toString());
	}
	else{
	    value = new BigDecimal((double) n.doubleValue());
	}
    }

    public MobyDataFloat(Number n, Registry r){
	this("", n, r);
    }

    public MobyDataFloat(Number n){
	this("", n, null);
    }

    /**
     * Constructor to use if the incoming value is a primitive.
     * If you want to pass in a float or int, cast it to a double.
     */
    public MobyDataFloat(String articleName, double d){
	this(articleName, d, null);
    }

    public MobyDataFloat(String articleName, double d, Registry registry){
	super(articleName);
	setDataType(MobyDataType.getDataType(MobyTags.MOBYFLOAT, registry));
	value = new BigDecimal(d);
    }

    public MobyDataFloat(double d, Registry r){
	this("", d, r);
    }

    public MobyDataFloat(double d){
	this("", d, null);
    }

    /**
     * Constructor to use if the incoming value is a string representing a number 
     * (including mantissa/exponent format).
     *
     * @throws NumberFormatException if the string does not represent a number
     */
    public MobyDataFloat(String articleName, String stringNumber) throws NumberFormatException{
	this(articleName, stringNumber, null);
    }

    public MobyDataFloat(String articleName, String stringNumber, Registry registry) throws NumberFormatException{
	super(articleName, registry);
	setDataType(MobyDataType.getDataType(MobyTags.MOBYFLOAT, registry));
	value = new BigDecimal(stringNumber.trim());
    }

    public MobyDataFloat(String stringNumber){
	this("", stringNumber, null);
    }

    public MobyDataFloat(String stringNumber, Registry registry){
	this("", stringNumber, registry);
    }

    public MobyDataFloat clone(){
	MobyDataFloat copy = new MobyDataFloat(getName(), value, getDataType().getRegistry());
	copy.setDataType(getDataType());
	copy.setId(getId());
	copy.setNamespaces(getNamespaces());
	return copy;
    }

    /**
     * Add a specified number to the data value.
     */
    public void add(Number n){
	if(n instanceof BigInteger){
	    value = value.add(new BigDecimal((BigInteger) n));
	}
	else if(n instanceof BigDecimal){
	    value = value.add((BigDecimal) n);
	}
	else{
	    value = value.add(new BigDecimal(""+n));
	}	
    }

    /**
     * Subtract a specified number from the data value.
     */
    public void subtract(Number n){
	if(n instanceof BigInteger){
	    value = value.subtract(new BigDecimal((BigInteger) n));
	}
	else if(n instanceof BigDecimal){
	    value = value.subtract((BigDecimal) n);
	}
	else{
	    value = value.subtract(new BigDecimal(""+n));
	}	
    }

    /**
     * Multiply the data value by a specified number.
     */
    public void multiply(Number n){
	if(n instanceof BigInteger){
	    value = value.multiply(new BigDecimal((BigInteger) n));
	}
	else if(n instanceof BigDecimal){
	    value = value.multiply((BigDecimal) n);
	}
	else{
	    value = value.multiply(new BigDecimal(""+n));
	}	
    }

    /**
     * Divide the data value by a number n, using the rounding off 
     * mode BigDecimal.ROUND_HALF_EVEN.
     */
    public void divide(Number n){
	divide(n, BigDecimal.ROUND_HALF_EVEN);
    }

    /**
     * Divide the data value by a number n, using the rounding off mode
     * of your choice from BigDecimal.
     */
    public void divide(Number n, int mode){
	if(n instanceof BigInteger){
	    value = value.divide(new BigDecimal((BigInteger) n), mode);
	}
	else if(n instanceof BigDecimal){
	    value = value.divide((BigDecimal) n, mode);
	}
	else{
	    value = value.divide(new BigDecimal(""+n), mode);
	}
    }

    /**
     * Raise the stored value to an exponent.
     * Warning, this method fails to produce a valid answer under four conditions, 
     * because I haven't had time to implement them properly.  If the exponent
     * is <b>non-integer</b> and:
     *
     * 1) The base is larger than Double.MAX_VALUE (or smaller Double.MIN_VALUE) 
     *
     * 2) The base is smaller then Double.MAX_VALUE (or smaller Double.MIN_VALUE)
     *    and the result of the operation is larger then (Double.MAX_VALUE or smaller Double.MIN_VALUE)
     *
     * 3) Underflows are silently ignored.
     *
     * 4) The exponent is greater than Long.MAX_VALUE (are you crazy???)
     *    
     */
    public void pow(Number n){
	boolean powerIsInteger = n.longValue() == n.doubleValue();
	double ourDoubleValue = value.doubleValue();
	if(ourDoubleValue == Double.POSITIVE_INFINITY ||
	   ourDoubleValue == Double.NEGATIVE_INFINITY ||
	   powerIsInteger){

	    // Can only do integer powers ourselves
	    if(!powerIsInteger){
		// Print some warning message
		return;
	    }
	    long power = n.longValue();

	    // Special case
	    if(power == 0){
		value = new BigDecimal(1);
		return;
	    }
	    else if(power < 0){
		for(long i = 1; i >= power; i--){
		    divide(value);
		}
	    }
	    else{
		for(long i = 1; i <= power; i++){
		    multiply(value);
		}
	    }
	    
	}
	// Fractional power, let Java handle it if it can
	else{
	    double pow = StrictMath.pow(ourDoubleValue, n.doubleValue());
	    if(pow == Double.POSITIVE_INFINITY ||
	       pow == Double.NEGATIVE_INFINITY){
		// Print some warning message
		return;
	    }
	    value = new BigDecimal(pow);
	}
    }   

    /**
     * @return a BigDecimal 
     */
    public Object getObject(){
	return value;
    }
    /**
     * A convenience method, which returns the underlying BigInteger's primitive int representation.
     * Caution: if the number is too big to fit, you'll end up with Double.NEGATIVE_INFINITY or Double.POSITIVE_INFINITY
     * Caution, you may lose precision on the data, even if it is within the range, especially for large e-value from BLAST, etc.
     * Use the BigDecimal returned by getObject if you aren't sure of the range of your input.
     */
    public double doubleValue(){
	return value.doubleValue();
    }

    public String getValue(){
	return ""+value;
    }

    /**
     * Override BigDecimal.compareTo() because it only compares to other BigInteger.  This
     * method will compare to any java.lang.Number
     */
    public int compareTo(Object object){
	if(object instanceof BigInteger){
	    return value.compareTo(new BigDecimal((BigInteger) object));
	}
	else if(object instanceof BigDecimal){
	    return value.compareTo((BigDecimal) object);
	}
	else if(object instanceof MobyDataObject){
	    return compareTo(((MobyDataObject) object).getObject());
	}
	// For any other number, unless our number is less than the maximum size of a long,
	// and less than the minimum long, there is no way they could be equal
	else if(object instanceof Number){
	    // Bigger than any long?
	    if(value.compareTo(new BigDecimal(""+Double.MAX_VALUE)) > 0){
		return 1;
	    }
	    // Smaller than any long?
	    else if(value.compareTo(new BigDecimal(""+Double.MIN_VALUE)) < 0){
		return -1;
	    }
	    // In the long range then
	    else{
		double diff = (value.doubleValue() - ((Number) object).doubleValue());
		// Three conditions to return int and avoid loss-of-precision warnings
		// that would be caused by returning int.
		if(diff == 0){
		    return 0;
		}
		else if(diff < 0){
		    return -1;
		}
		else{
		    return 1;
		}
	    }
	}
	// Cannot compare, always return -1
	else{
	    return -1;
	}	
    }

    /**
     * Override BigDecimal.equals() because it only compares to other BigInteger.  This
     * method will compare to any java.lang.Number
     */
    public boolean equals(Object object){
	if(object instanceof BigDecimal){
	    return value.equals(object);
	}
	else if(object instanceof BigInteger){
	    return value.equals(new BigDecimal((BigInteger) object));
	}
	else if(object instanceof MobyDataObject){
	    return equals(((MobyDataObject) object).getObject());
	}
	// For any other number, unless our number is less than the maximum size of a long,
	// and less than the minimum long, there is no way they could be equal
	else if(object instanceof Number && 
		value.compareTo(new BigDecimal(""+Double.MAX_VALUE)) <= 0 &&
		value.compareTo(new BigDecimal(""+Double.MIN_VALUE)) >= 0){
	    return value.doubleValue() == ((Number) object).doubleValue();
	}
	else{
	    return false;
	}
    }

    public String toXML(){
	MobyNamespace[] ns = getNamespaces();
	if(xmlMode == MobyDataInstance.SERVICE_XML_MODE){
	  return "<" + MobyTags.MOBYFLOAT + " " + getAttrXML() + ">" + 
	      value.toString() + "</" + MobyTags.MOBYFLOAT + ">";
        }
	else{
	    return super.toXML();
	}
    }
}
