/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2001-06 Wolfgang M. Meier
 *  wolfgang@exist-db.org
 *  http://exist.sourceforge.net
 *  
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2
 *  of the License, or (at your option) any later version.
 *  
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *  
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *  
 *  $Id: YearMonthDurationValue.java 4590 2006-10-14 10:24:31Z brihaye $
 */

package org.exist.xquery.value;

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

import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.Duration;

import org.exist.util.FastStringBuffer;
import org.exist.xquery.XPathException;

/**
 * @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
 */
public class YearMonthDurationValue extends OrderedDurationValue {

	public static final Duration CANONICAL_ZERO_DURATION =
		TimeUtils.getInstance().newDuration(true, null, BigInteger.ZERO, null, null, null, null);
	
	YearMonthDurationValue(Duration duration) throws XPathException {	
		super(duration);
		//Take care : not the same one than above !
		//TODO : sort this out ! test case :
		//xs:yearMonthDuration("P1000Y6M") + xs:yearMonthDuration("P0Y0M")  
		if (!duration.equals(DurationValue.CANONICAL_ZERO_DURATION)) {
			if (duration.isSet(DatatypeConstants.DAYS) ||		
				duration.isSet(DatatypeConstants.HOURS) ||
				duration.isSet(DatatypeConstants.MINUTES) ||
				//Always set !
				//!duration.getField(DatatypeConstants.SECONDS).equals(BigInteger.ZERO))
				duration.isSet(DatatypeConstants.SECONDS))
				throw new XPathException("The value '" + duration + "' is not an " + Type.getTypeName(getType()) + 
						" since it specifies days, hours, minutes or seconds values");
		}		
	}

	public YearMonthDurationValue(String str) throws XPathException {
		this(createDurationYearMonth(str));		
	}
	
	private static Duration createDurationYearMonth(String str) throws XPathException {
		try {
			return TimeUtils.getInstance().newDurationYearMonth(StringValue.trimWhitespace(str));
		} catch (IllegalArgumentException e) {
			throw new XPathException("FORG0001: cannot construct " + Type.getTypeName(Type.YEAR_MONTH_DURATION) +
					" from \"" + str + "\"");            
		}
	}
	
	protected Duration canonicalZeroDuration() {
		return CANONICAL_ZERO_DURATION;
	}
	
	public int getValue() {
		return duration.getSign() * (duration.getYears() * 12 + duration.getMonths());
	}

	public int getType() {
		return Type.YEAR_MONTH_DURATION;
	}
	
	public String getStringValue() {
        FastStringBuffer sb = new FastStringBuffer(32);
        if (getCanonicalDuration().getSign() < 0) {
            sb.append('-');
        }
        sb.append('P');
        if (getCanonicalDuration().getYears() != 0) {
            sb.append(getCanonicalDuration().getYears() + "Y");
        }
        if (getCanonicalDuration().getMonths() !=0 || getCanonicalDuration().getYears() == 0) {
            sb.append(getCanonicalDuration().getMonths() + "M");
        }
        return sb.toString();
	}	

	public AtomicValue convertTo(int requiredType) throws XPathException {
		switch (requiredType) {
			case Type.ITEM :
			case Type.ATOMIC :
			case Type.YEAR_MONTH_DURATION :
				return this;
			case Type.STRING :
				return new StringValue(getStringValue());
			case Type.DURATION :
				return new DurationValue(TimeUtils.getInstance().newDuration(
						duration.getSign() >= 0,
						(BigInteger) duration.getField(DatatypeConstants.YEARS),
						(BigInteger) duration.getField(DatatypeConstants.MONTHS),
						null, null, null, null
				));
			case Type.DAY_TIME_DURATION:
				return new DayTimeDurationValue(DayTimeDurationValue.CANONICAL_ZERO_DURATION);
			//case Type.DOUBLE:
				//return new DoubleValue(monthsValueSigned().doubleValue());
				//return new DoubleValue(Double.NaN);
			//case Type.DECIMAL:
				//return new DecimalValue(monthsValueSigned().doubleValue());
			case Type.UNTYPED_ATOMIC :
				return new UntypedAtomicValue(getStringValue());				
			default :
				throw new XPathException(
					"XPTY0004: cannot cast 'xs:yearMonthDuration(\"" + getStringValue() + 
					"\")' to " + Type.getTypeName(requiredType));
		}
	}

	protected DurationValue createSameKind(Duration dur) throws XPathException {
		return new YearMonthDurationValue(dur);
	}
	
	public ComputableValue plus(ComputableValue other) throws XPathException {
		try {
			if (other.getType() == Type.TIME) throw new IllegalArgumentException();
			return super.plus(other);
		} catch (IllegalArgumentException e) {
			throw new XPathException(
					"Operand to plus should be of type xdt:yearMonthDuration, xs:date, "
						+ "or xs:dateTime; got: "
						+ Type.getTypeName(other.getType()));
		}
	}
	
	public ComputableValue mult(ComputableValue other) throws XPathException {
		BigDecimal factor = numberToBigDecimal(other, "Operand to mult should be of numeric type; got: ");
		boolean isFactorNegative = factor.signum() < 0;		
		YearMonthDurationValue product = fromDecimalMonths(
			new BigDecimal(monthsValueSigned())
			.multiply(factor.abs())
			.setScale(0, BigDecimal.ROUND_HALF_UP)
		);
		if (isFactorNegative)
			return product.negate();
		return product;
	}

	public ComputableValue div(ComputableValue other) throws XPathException {
		if (other.getType() == Type.YEAR_MONTH_DURATION) {
			return new IntegerValue(getValue()).div(new IntegerValue(((YearMonthDurationValue) other).getValue()));
		}
		if (other instanceof NumericValue) {
			if (((NumericValue)other).isNaN()) {
				throw new XPathException("FOCA0005: Operand is not a number");				
			}
			if (((NumericValue)other).isInfinite()) {
				return new YearMonthDurationValue("P0M");
			}
		}
		BigDecimal divisor = numberToBigDecimal(other, "Can not divide xdt:yearMonthDuration by '" + Type.getTypeName(other.getType())+ "'");
		boolean isDivisorNegative = divisor.signum() < 0;
		YearMonthDurationValue quotient = fromDecimalMonths(
			new BigDecimal(monthsValueSigned())
			.divide(divisor.abs(), 20, BigDecimal.ROUND_HALF_EVEN));		
		if (isDivisorNegative)
			return quotient.negate();
		return new YearMonthDurationValue(quotient.getCanonicalDuration());	
	}
	
	private YearMonthDurationValue fromDecimalMonths(BigDecimal x) throws XPathException {
		return new YearMonthDurationValue(TimeUtils.getInstance().newDurationYearMonth(
				x.signum() >= 0, null, x.toBigInteger()));
	}
	
    public boolean effectiveBooleanValue() throws XPathException {
        throw new XPathException("FORG0006: value of type " + Type.getTypeName(getType()) +
            " has no boolean value.");
    }
}
