/**
{{IS_NOTE
	Purpose:
		
	Description:
		
	History:
		Apr 30, 2014, Created by henrichen
}}IS_NOTE

Copyright (C) 2014 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
}}IS_RIGHT
 */
package org.zkoss.poi.ss.util;

import java.util.Locale;

import org.zkoss.poi.ss.format.Formatters;
import org.zkoss.poi.ss.util.ExpandedDouble;
import org.zkoss.poi.ss.util.NormalisedDecimal;

/**
 * Convert a number to "General" formatting text.
 * @author henrichen
 * @since 3.5.0
 */
public class NumberToGeneralTextConverter {
	//20140428, henrichen@zkoss.org: ZSS-666
	//maxLen is the column width in number of characters
	public static String toGeneralText(double value, Locale locale, int maxLen) {
		if (maxLen <= 0) return "";
		
		return rawDoubleBitsToGeneralText(Double.doubleToLongBits(value), locale, maxLen); 
	}
	
	//20140428, henrichen@zkoss.org: ZSS-666
	/* package */ static String rawDoubleBitsToGeneralText(long pRawBits, Locale locale, int maxLen) {

		long rawBits = pRawBits;
		boolean isNegative = rawBits < 0; // sign bit is in the same place for long and double
		if (isNegative) {
			rawBits &= 0x7FFFFFFFFFFFFFFFL;
		}
		if (rawBits == 0) {
			return isNegative ? "-0" : "0";
		}
		ExpandedDouble ed = new ExpandedDouble(rawBits);
		if (ed.getBinaryExponent() < -1022) {
			// value is 'denormalised' which means it is less than 2^-1022
			// excel displays all these numbers as zero, even though calculations work OK
			return isNegative ? "-0" : "0";
		}
		//maxLen - 1 to reserve minus sign position when maxLen < 12; otherwise, 11
		maxLen = maxLen >= 12 ? 11 : isNegative ? maxLen - 1 : maxLen;
		final char dot = Formatters.getDecimalSeparator(locale);
		if (ed.getBinaryExponent() == 1024) {
			// Special number NaN /Infinity
			// Normally one would not create HybridDecimal objects from these values
			// except in these cases Excel really tries to render them as if they were normal numbers
			if(rawBits == NumberToTextConverter.EXCEL_NAN_BITS) {
				return convertToGeneralText("3" + dot + "484840871308", "+308", 308, maxLen, dot, false);
			}
			// This is where excel really gets it wrong
			// Special numbers like Infinity and NaN are interpreted according to
			// the standard rules below.
			isNegative = false; // except that the sign bit is ignored
		}
		NormalisedDecimal nd = ed.normaliseBaseTen();
		NormalisedDecimal rnd = nd.roundUnits();
		int decExponent = rnd.getDecimalExponent();
		String decimalDigits;
		if (Math.abs(decExponent)>98) {
			decimalDigits = rnd.getSignificantDecimalDigitsLastDigitRounded();
			if (decimalDigits.length() == 16) {
				// rounding caused carry
				decExponent++;
			}
		} else {
			decimalDigits = rnd.getSignificantDecimalDigits();
		}
		int countSigDigits = NumberToTextConverter.countSignifantDigits(decimalDigits);
		decimalDigits = decimalDigits.substring(0, countSigDigits);
		decimalDigits = ""+decimalDigits.charAt(0) + dot + (decimalDigits.length() > 1 ? decimalDigits.substring(1) : "");
		return convertToGeneralText(decimalDigits, getExp(decExponent), decExponent, maxLen, dot, isNegative);
	}
	
	//20140428, henrichen@zkoss.org: ZSS-666
	private static String getExp(int decExponent) {
		final StringBuilder sb = new StringBuilder(5);
		if (decExponent < 0) {
			sb.append('-');
			NumberToTextConverter.appendExp(sb, -decExponent);
		} else {
			sb.append('+');
			NumberToTextConverter.appendExp(sb, decExponent);
		}
		return sb.toString();
	}
	
	//20140428, henrichen@zkoss.org: ZSS-666
	private static String convertToGeneralText(String base, String exp, int e, int maxLen, char dot, boolean isNegative) {
		final int blen = base.length();
		final int nExpDigits = exp.length() + 1;
		int nExpSpace = maxLen - nExpDigits; //space to hold digits in scientific notation
		if (nExpSpace > 1) --nExpSpace; //minus the '.'(dot) 
		if (e < 0) {
			final int nLeadingZeros = -e - 1;
			final int nSignificantDigits = blen - 1;
			final int nLiteralSpace = maxLen - 2 - nLeadingZeros; //space to hold digits in literal notation, "0." occupy 2 characters
			
			//when enough space for significant value; 
			//or can show more significant digits in literal notation then in scientific notation
			if (nLiteralSpace >= nSignificantDigits || (nLiteralSpace > 0 && nLiteralSpace >= nExpSpace)) { 
				return literalNotation(base, exp, blen, e, maxLen, dot, isNegative);
			} else if (nExpSpace > 0) {
				return scientificNotation(base, exp, e, maxLen, dot, isNegative);
			} else if (maxLen > 0 && nExpSpace < 0) {
				if (maxLen < (nLeadingZeros + 2)) {
					return isNegative ? "-0" : "0";
				} else {
					return literalNotation(base, exp, blen, e, maxLen, dot, isNegative);
				}
			} else {
				return overflowNotation(isNegative ? maxLen + 1 : maxLen);
			}
		} else if (e >= maxLen) {
			if (nExpSpace > 0) { 
				return scientificNotation(base, exp, e, maxLen, dot, isNegative);
			} else {
				return overflowNotation(isNegative ? maxLen + 1 : maxLen);
			}
		} else { //literal form
			return literalNotation(base, exp, blen, e, maxLen, dot, isNegative);
		}
	}
	
	//20140428, henrichen@zkoss.org: ZSS-666
	private static String overflowNotation(int maxLen) {
		final StringBuilder sb = new StringBuilder(maxLen);
		for (int j = maxLen; j > 0; --j) {
			sb.append('#');
		}
		return sb.toString();
	}
	
	//20140428, henrichen@zkoss.org: ZSS-666
	private static String scientificNotation(String base, String exp, int e, int maxLen, char dot, boolean isNegative) {
		StringBuilder sb = new StringBuilder(maxLen+1);
		if (isNegative) sb.append('-');
		//subtract space hold by exponential part
		final int roundIndex = maxLen - exp.length() - 1;
		final String newbase = carryDigits(base, roundIndex, dot);
		return sb.append(newbase).append('E').append(exp).toString();
	}
	
	//20140428, henrichen@zkoss.org: ZSS-666
	private static String literalNotation(String base, String exp, int blen, int e, int maxLen, char dot, boolean isNegative) {
		StringBuilder sb = new StringBuilder(maxLen+1);
		if (isNegative) sb.append('-');
		if (e < 0) {
			int nLeadingZeros = -e - 1;
			sb.append("0"+dot);
			for (int j = nLeadingZeros; j > 0; --j) {
				sb.append('0');
			}
			sb.append(base.charAt(0)).append(base.substring(2));
		} else {
			int dotPos = 1 + e;
			if (blen > (dotPos + 1)) { // dot within number, move dot
				if (dotPos == 1) { //special case, no move
					sb.append(base);
				} else {
					sb.append(base.charAt(0)).append(base.substring(2, dotPos+1)).append(dot).append(base.substring(dotPos+1));
				}
			} else { // append zeros
				int nAppendZeros = dotPos + 1 - blen;
				sb.append(base.charAt(0)).append(base.substring(2));
				for (int j = nAppendZeros; j > 0; --j) {
					sb.append('0');
				}
			}
		}
		return carryDigits(sb.toString(), isNegative ? maxLen + 1 : maxLen, dot);
	}
	
	final static private int ZERO = Character.getNumericValue('0'); 
	//20140428, henrichen@zkoss.org: ZSS-666
	private static String carryDigits(String v, int roundIndex, char dot) {
		if (v.length() <= roundIndex) return v;
		
		boolean withDot = false;
		if (v.charAt(roundIndex) >= '5') { //carry in?
			char[] list0 = new char[roundIndex];
			boolean carry = true;
			for (int k = roundIndex; --k >= 0;) {
				final char ch = v.charAt(k);
				if (ch != dot) {
					if (carry) {
						if (ch == '9') {
							list0[k] = '0';
							carry = true;
						} else {
							list0[k] = Character.forDigit(Character.getNumericValue(ch) + 1 - ZERO, 10);
							carry = false;
						}
						continue;
					} 
				}  else {
					withDot = true;
				}
				list0[k] = ch;
			}
			v = String.valueOf(list0);
		}
		
		return removeTailZeros(v, roundIndex, withDot, dot);
	}
	
	//20140428, henrichen@zkoss.org: ZSS-666
	// Removes tail zeros after decimal point; also remove decimal point itself
	// if number with integer part only
	static String removeTailZeros(String v, int roundIndex, boolean withDot, char dot) {
		int r = -1;
		//remove tail zeros and dot if required
		for (int j = roundIndex; --j >= 0;) {
			final char ch = v.charAt(j);
			if (ch == dot) {
				withDot = true;
				if (r < 0) r = j;
				break;
			} else if (ch != '0' && r < 0) {
				r = j + 1;
				if (withDot) break;
			}
	    }
	    return v.substring(0, withDot ? r : roundIndex);
	}	
}
