package org.biomoby.shared.data;

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

import java.math.BigDecimal;
import java.math.BigInteger;

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

public class MobyDataInt extends MobyDataObject{

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

    public MobyDataInt(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, BigInteger, BigDecimal, etc.
     * Real numbers will be converted to their integer equivalents. 
     */
    public MobyDataInt(String articleName, Number n){
	this(articleName, n, null);
    }

    public MobyDataInt(String articleName, Number n, Registry registry){
	super(articleName, registry);
	setDataType(MobyDataType.getDataType(MobyTags.MOBYINTEGER, registry));
	if(n instanceof BigInteger){
	    value = (BigInteger) n;
	}
	else if(n instanceof BigDecimal){
	    value = ((BigDecimal) n).toBigInteger();
	}
	else{
	    value = new BigInteger("" + n.intValue());
	}
    }

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

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

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

    public MobyDataInt(String articleName, int i, Registry registry){
	super(articleName, registry);
	setDataType(MobyDataType.getDataType(MobyTags.MOBYINTEGER, registry));
	value = new BigInteger(""+i);
    }

    public MobyDataInt(int i){
	this("", i);
    }

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

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

    /**
     * Perform the addition operand on the data value.  Non-integer arguments
     * will be rounded to their BigInteger equivalents.
     */
    public void add(Number n){
	if(n instanceof BigInteger){
	    value = value.add((BigInteger) n);
	}
	else if(n instanceof BigDecimal){
	    value = value.add(((BigDecimal) n).toBigInteger());
	}
	else{
	    value = value.add((new BigDecimal(n.doubleValue())).toBigInteger());
	}	
    }

    /**
     * Perform the subtraction operand on the data value.  Non-integer arguments
     * will be rounded to their BigInteger equivalents.
     */
    public void subtract(Number n){
	if(n instanceof BigInteger){
	    value = value.subtract((BigInteger) n);
	}
	else if(n instanceof BigDecimal){
	    value = value.subtract(((BigDecimal) n).toBigInteger());
	}
	else{
	    value = value.subtract((new BigDecimal(n.doubleValue())).toBigInteger());
	}	
    }

    /**
     * Perform the multiplication operand on the data value.  Non-integer arguments
     * will be rounded to their BigInteger equivalents.
     */
    public void multiply(Number n){
	if(n instanceof BigInteger){
	    value = value.multiply((BigInteger) n);
	}
	else if(n instanceof BigDecimal){
	    value = value.multiply(((BigDecimal) n).toBigInteger());
	}
	else{
	    value = value.multiply((new BigDecimal(n.doubleValue())).toBigInteger());
	}	
    }

    /**
     * Perform the division operand on the data value.  Non-integer arguments
     * will be rounded to their BigInteger equivalents.
     */
    public void divide(Number n){
	if(n instanceof BigInteger){
	    value = value.divide((BigInteger) n);
	}
	else if(n instanceof BigDecimal){
	    value = value.divide(((BigDecimal) n).toBigInteger());
	}
	else{
	    value = value.divide((new BigDecimal(n.doubleValue())).toBigInteger());
	}
    }

    /**
     * Perform the modulus operand on the data value.  Non-integer arguments
     * will be rounded to their BigInteger equivalents.
     */
    public void mod(Number n){
	if(n instanceof BigInteger){
	    value = value.mod((BigInteger) n);
	}
	else if(n instanceof BigDecimal){
	    value = value.mod(((BigDecimal) n).toBigInteger());
	}
	else{
	    value = value.mod((new BigDecimal(n.doubleValue())).toBigInteger());
	}
    }

    /**
     * Raise the stored value to an exponent.
     * Non-integer arguments will be rounded to their long equivalents.
     * If your exponent is greater than Long.MAX_VALUE, you are crazy
     * (as computing this would require the energy to 
     * <a href="http://www.fastcompany.com/magazine/88/debunk.html">boil 
     * earth's oceans</a>), and out of luck.
     */
    public void pow(Number n){
	long power = n.longValue();
	
	// Special case
	if(power == 0){
	    value = BigInteger.ONE;
	    return;
	}
	else if(power < 0){
	    for(long i = 1; i >= power; i--){
		divide(value);
	    }
	}
	else{
	    for(long i = 1; i <= power; i++){
		multiply(value);
	    }
	}
    }   

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

    /**
     * Override BigInteger.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((BigInteger) object);
	}
	else if(object instanceof BigDecimal){
	    return value.compareTo(((BigDecimal) object).toBigInteger());
	}
	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 BigInteger(""+Long.MAX_VALUE)) > 0){
		return 1;
	    }
	    // Smaller than any long?
	    else if(value.compareTo(new BigInteger(""+Long.MIN_VALUE)) < 0){
		return -1;
	    }
	    // In the long range then
	    else{
		long diff = (value.longValue() - ((Number) object).longValue());
		// 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;
	}	
    }

    /**
     * @return a BigInteger, the full precision value of the data
     */
    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're only getting the lower 32 bits!
     * Use the BigInteger returned by getObject if you aren't sure of the range of your input.
     */
    public int intValue(){
	return value.intValue();
    }

    /**
     * A convenience method, which returns the underlying BigInteger's primitive int representation.
     * Caution: if the number is too big to fit, you're only getting the lower 64 bits!
     * Use the BigInteger returned by getObject if you aren't sure of the range of your input.
     */
    public long longValue(){
	return value.longValue();
    }

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

    public String toXML(){
	if(xmlMode == MobyDataInstance.SERVICE_XML_MODE){
	    return "<" + MobyTags.MOBYINTEGER + " " + getAttrXML() + ">" + 
		value.toString() + "</" + MobyTags.MOBYINTEGER + ">";
	}
	else{
	    return super.toXML();
	}
    }
}
