//
// typica - A client library for Amazon Web Services
// Copyright (C) 2007 Xerox Corporation
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// NOTE: the original code came from the Amazon SimpleDB java client.
//       The original copywrite notice is included below
//

/******************************************************************************* 
 *  Copyright 2007 Amazon Technologies, Inc.  
 *  Licensed under the Apache License, Version 2.0 (the "License"); 
 *  
 *  You may not use this file except in compliance with the License. 
 *  You may obtain a copy of the License at: http://aws.amazon.com/apache2.0
 *  This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 
 *  CONDITIONS OF ANY KIND, either express or implied. See the License for the 
 *  specific language governing permissions and limitations under the License.
 * ***************************************************************************** 
 *    __  _    _  ___ 
 *   (  )( \/\/ )/ __)
 *   /__\ \    / \__ \
 *  (_)(_) \/\/  (___/
 * 
 *  Amazon Simple DB Java Library
 *  API Version: 2007-11-07
 *  Generated: Fri Jan 18 01:13:17 PST 2008 
 * 
 */

package com.xerox.amazonws.sdb;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Provides collection of static functions for conversion of various values into strings that may be 
 * compared lexicographically.
 */
public class DataUtils {

    /** static value hardcoding date format used for conversation of Date into String */
    private static String dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";

    /**
     * Encodes positive integer value into a string by zero-padding number up to the specified number of digits.
     * 
     * @param	number positive integer to be encoded
     * @param	maxNumDigits maximum number of digits in the largest value in the data set
     * @return string representation of the zero-padded integer
     */
    public static String encodeZeroPadding(int number, int maxNumDigits) {
        String integerString = Integer.toString(number);
        int numZeroes = maxNumDigits - integerString.length();
        StringBuffer strBuffer = new StringBuffer(numZeroes + integerString.length());
        for (int i = 0; i < numZeroes; i++) {
            strBuffer.insert(i, '0');
        }
        strBuffer.append(integerString);
        return strBuffer.toString();
    }

    /**
     * Encodes positive long value into a string by zero-padding number up to the specified number of digits.
     * 
     * @param	number positive long to be encoded
     * @param	maxNumDigits maximum number of digits in the largest value in the data set
     * @return string representation of the zero-padded long
     */
    public static String encodeZeroPadding(long number, int maxNumDigits) {
        String longString = Long.toString(number);
        int numZeroes = maxNumDigits - longString.length();
        StringBuffer strBuffer = new StringBuffer(numZeroes + longString.length());
        for (int i = 0; i < numZeroes; i++) {
            strBuffer.insert(i, '0');
        }
        strBuffer.append(longString);
        return strBuffer.toString();
    }

    /**
     * Encodes positive float value into a string by zero-padding number up to the specified number of digits
     * 
     * @param	number positive float value to be encoded
     * @param	maxNumDigits	maximum number of digits preceding the decimal point in the largest value in the data set
     * @return string representation of the zero-padded float value
     */
    public static String encodeZeroPadding(float number, int maxNumDigits) {
        String floatString = Float.toString(number);
        int numBeforeDecimal = floatString.indexOf('.');
        numBeforeDecimal = (numBeforeDecimal >= 0 ? numBeforeDecimal : floatString.length());
        int numZeroes = maxNumDigits - numBeforeDecimal;
        StringBuffer strBuffer = new StringBuffer(numZeroes + floatString.length());
        for (int i = 0; i < numZeroes; i++) {
            strBuffer.insert(i, '0');
        }
        strBuffer.append(floatString);
        return strBuffer.toString();
    }

    /**
     * Encodes positive double value into a string by zero-padding number up to the specified number of digits
     * 
     * @param	number positive double value to be encoded
     * @param	maxNumDigits	maximum number of digits preceding the decimal point in the largest value in the data set
     * @return string representation of the zero-padded double value
     */
    public static String encodeZeroPadding(double number, int maxNumDigits) {
        String doubleString = Double.toString(number);
        int numBeforeDecimal = doubleString.indexOf('.');
        numBeforeDecimal = (numBeforeDecimal >= 0 ? numBeforeDecimal : doubleString.length());
        int numZeroes = maxNumDigits - numBeforeDecimal;
        StringBuffer strBuffer = new StringBuffer(numZeroes + doubleString.length());
        for (int i = 0; i < numZeroes; i++) {
            strBuffer.insert(i, '0');
        }
        strBuffer.append(doubleString);
        return strBuffer.toString();
    }

    /**
     * Decodes zero-padded positive integer value from the string representation
     * 
     * @param value zero-padded string representation of the integer
     * @return original integer value
     */
    public static int decodeZeroPaddingInt(String value) {
        return Integer.parseInt(value, 10);
    }

    /**
     * Decodes zero-padded positive long value from the string representation
     * 
     * @param value zero-padded string representation of the long
     * @return original long value
     */
    public static long decodeZeroPaddingLong(String value) {
        return Long.parseLong(value, 10);
    }

    /**
     * Decodes zero-padded positive float value from the string representation
     * 
     * @param value zero-padded string representation of the float value
     * @return original float value
     */
    public static float decodeZeroPaddingFloat(String value) {
        return Float.valueOf(value).floatValue();
    }

    /**
     * Decodes zero-padded positive double value from the string representation
     * 
     * @param value zero-padded string representation of the double value
     * @return original double value
     */
    public static double decodeZeroPaddingDouble(String value) {
        return Double.valueOf(value).doubleValue();
    }

    /**
     * Encodes real integer value into a string by offsetting and zero-padding 
     * number up to the specified number of digits.  Use this encoding method if the data
     * range set includes both positive and negative values.
     * 
     * @param number integer to be encoded
     * @param maxNumDigits maximum number of digits in the largest absolute value in the data set
     * @param offsetValue offset value, has to be greater than absolute value of any negative number in the data set.
     * @return string representation of the integer
     */
    public static String encodeRealNumberRange(int number, int maxNumDigits, int offsetValue) {
        long offsetNumber = number + offsetValue;
        String longString = Long.toString(offsetNumber);
        int numZeroes = maxNumDigits - longString.length();
        StringBuffer strBuffer = new StringBuffer(numZeroes + longString.length());
        for (int i = 0; i < numZeroes; i++) {
            strBuffer.insert(i, '0');
        }
        strBuffer.append(longString);
        return strBuffer.toString();
    }

    /**
     * Encodes real long value into a string by offsetting and zero-padding 
     * number up to the specified number of digits.  Use this encoding method if the data
     * range set includes both positive and negative values.
     * 
     * @param number long to be encoded
     * @param maxNumDigits maximum number of digits in the largest absolute value in the data set
     * @param offsetValue offset value, has to be greater than absolute value of any negative number in the data set.
     * @return string representation of the long
     */
    public static String encodeRealNumberRange(long number, int maxNumDigits, int offsetValue) {
        long offsetNumber = number + offsetValue;
        String longString = Long.toString(offsetNumber);
        int numZeroes = maxNumDigits - longString.length();
        StringBuffer strBuffer = new StringBuffer(numZeroes + longString.length());
        for (int i = 0; i < numZeroes; i++) {
            strBuffer.insert(i, '0');
        }
        strBuffer.append(longString);
        return strBuffer.toString();
    }

    /**
     * Encodes real float value into a string by offsetting and zero-padding 
     * number up to the specified number of digits.  Use this encoding method if the data
     * range set includes both positive and negative values.
     * 
     * @param number float to be encoded
     * @param maxDigitsLeft maximum number of digits left of the decimal point in the largest absolute value in the data set
     * @param maxDigitsRight maximum number of digits right of the decimal point in the largest absolute value in the data set, i.e. precision
     * @param offsetValue offset value, has to be greater than absolute value of any negative number in the data set.
     * @return string representation of the integer
     */
    public static String encodeRealNumberRange(float number, int maxDigitsLeft, int maxDigitsRight, int offsetValue) {
        long shiftMultiplier = (long) Math.pow(10, maxDigitsRight);
        long shiftedNumber = (long) Math.round(number * shiftMultiplier);
        long shiftedOffset = offsetValue * shiftMultiplier;
        long offsetNumber = shiftedNumber + shiftedOffset;
        String longString = Long.toString(offsetNumber);
        int numBeforeDecimal = longString.length();
        int numZeroes = maxDigitsLeft + maxDigitsRight - numBeforeDecimal;
        StringBuffer strBuffer = new StringBuffer(numZeroes + longString.length());
        for (int i = 0; i < numZeroes; i++) {
            strBuffer.insert(i, '0');
        }
        strBuffer.append(longString);
        return strBuffer.toString();
    }

    /**
     * Encodes real double value into a string by offsetting and zero-padding 
     * number up to the specified number of digits.  Use this encoding method if the data
     * range set includes both positive and negative values.
     * 
     * @param number double to be encoded
     * @param maxDigitsLeft maximum number of digits left of the decimal point in the largest absolute value in the data set
     * @param maxDigitsRight maximum number of digits right of the decimal point in the largest absolute value in the data set, i.e. precision
     * @param offsetValue offset value, has to be greater than absolute value of any negative number in the data set.
     * @return string representation of the integer
     */
    public static String encodeRealNumberRange(double number, int maxDigitsLeft, int maxDigitsRight, long offsetValue) {
        int shiftMultiplier = (int) Math.pow(10, maxDigitsRight);
        long shiftedNumber = (long) Math.round(number * shiftMultiplier);
        long shiftedOffset = offsetValue * shiftMultiplier;
        long offsetNumber = shiftedNumber + shiftedOffset;
        String longString = Long.toString(offsetNumber);
        int numBeforeDecimal = longString.length();
        int numZeroes = maxDigitsLeft + maxDigitsRight - numBeforeDecimal;
        StringBuffer strBuffer = new StringBuffer(numZeroes + longString.length());
        for (int i = 0; i < numZeroes; i++) {
            strBuffer.insert(i, '0');
        }
        strBuffer.append(longString);
        return strBuffer.toString();
    }

    /**
     * Decodes integer value from the string representation that was created by 
     * using encodeRealNumberRange(..) function.
     * 
     * @param value string representation of the integer value
     * @param offsetValue offset value that was used in the original encoding
     * @return original integer value
     */
    public static int decodeRealNumberRangeInt(String value, int offsetValue) {
        long offsetNumber = Long.parseLong(value, 10);
        return (int) (offsetNumber - offsetValue);
    }

    /**
     * Decodes long value from the string representation that was created by 
     * using encodeRealNumberRange(..) function.
     * 
     * @param value string representation of the long value
     * @param offsetValue offset value that was used in the original encoding
     * @return original long value
     */
    public static long decodeRealNumberRangeLong(String value, long offsetValue) {
        long offsetNumber = Long.parseLong(value, 10);
        return (long) (offsetNumber - offsetValue);
    }

    /**
     * Decodes float value from the string representation that was created by using encodeRealNumberRange(..) function.
     * 
     * @param value string representation of the integer value
     * @param maxDigitsRight maximum number of digits left of the decimal point in 
     * the largest absolute value in the data set (must be the same as the one used for encoding).
     * @param offsetValue offset value that was used in the original encoding
     * @return original float value
     */
    public static float decodeRealNumberRangeFloat(String value, int maxDigitsRight, int offsetValue) {
        long offsetNumber = Long.parseLong(value, 10);
        long shiftMultiplier = (long) Math.pow(10, maxDigitsRight);
        double tempVal = (double) (offsetNumber - offsetValue * shiftMultiplier);
        return (float) (tempVal / (double) (shiftMultiplier));
    }

    /**
     * Decodes double value from the string representation that was created by using encodeRealNumberRange(..) function.
     * 
     * @param value string representation of the integer value
     * @param maxDigitsRight maximum number of digits left of the decimal point in 
     * the largest absolute value in the data set (must be the same as the one used for encoding).
     * @param offsetValue offset value that was used in the original encoding
     * @return original double value
     */
    public static double decodeRealNumberRangeDouble(String value, int maxDigitsRight, long offsetValue) {
        long offsetNumber = Long.parseLong(value, 10);
        int shiftMultiplier = (int) Math.pow(10, maxDigitsRight);
        double tempVal = (double) (offsetNumber - offsetValue * shiftMultiplier);
        return (double) (tempVal / (double) (shiftMultiplier));
    }

    /**
     * Encodes date value into string format that can be compared lexicographically 
     * 
     * @param date date value to be encoded
     * @return string representation of the date value
     */
    public static String encodeDate(Date date) {
        SimpleDateFormat dateFormatter = new SimpleDateFormat(dateFormat);
        /* Java doesn't handle ISO8601 nicely: need to add ':' manually */
        String result = dateFormatter.format(date);
        return result.substring(0, result.length() - 2) + ":" + result.substring(result.length() - 2);
    }

    /**
     * Decodes date value from the string representation created using encodeDate(..) function.
     * 
     * @param	value	string representation of the date value
     * @return			original date value
     */
    public static Date decodeDate(String value) throws ParseException {
        String javaValue = value.substring(0, value.length() - 3) + value.substring(value.length() - 2);
        SimpleDateFormat dateFormatter = new SimpleDateFormat(dateFormat);
        return dateFormatter.parse(javaValue);
    }

	// the offset added to negative significands to yield proper collating order
	private static final BigDecimal SIGNIFICAND_COLLATOR = BigDecimal.TEN;
	
	// the offset used on certain exponents to yield proper collating order
	private static final int EXPONENT_COLLATOR = 999;
	
	private static final DecimalFormat FULL_DECIMAL_FORMAT = new DecimalFormat();
	static { FULL_DECIMAL_FORMAT.applyPattern("0.0000000000000000E000"); }
	
	private static final DecimalFormat SIGNIFICAND_FORMAT = new DecimalFormat();
	static { SIGNIFICAND_FORMAT.applyPattern("0.0000000000000000"); }
	
	/* A Java implementation of Doug Wood's work in progress
	 * "Directory string representation for floating point values" 
	 * http://tools.ietf.org/html/draft-wood-ldapext-float-00
	 * 
	 * Note: Section 3.5 of the above draft memo should be corrected to read:
	 * 3.5 Negative mantissa and positive exponent (case 1)
	 * When the exponent is positive and the mantissa are negative, the collating
	 * sequence is flipped for both of them.  This is achieved by subtracting
	 * the exponent from 999, and adding the mantissa to 10.
	 * 
	 * Note: the term 'significand' is used here rather than 'mantissa'.
	 * Infinity and NaN are not handled.
	 */
	public static String encodeDouble(double d) {
		// todo: replace String manipulation with math
		String decimalString = FULL_DECIMAL_FORMAT.format(d);
		int splitPoint = decimalString.indexOf('E');
		String significand = decimalString.substring(0, splitPoint);
		String exponent = decimalString.substring(splitPoint + 1);
		boolean negativeExponent = exponent.startsWith("-");
		String result;
		
		if (significand.startsWith("-")) {
			// BigDecimal here preserves significand's last digit during add()
			BigDecimal significandValue = new BigDecimal(significand);
			BigDecimal collatedSignificand = significandValue.add(SIGNIFICAND_COLLATOR);
			String formattedSignificand = SIGNIFICAND_FORMAT.format(collatedSignificand);
			
			if (!negativeExponent) { 
				int exponentValue = EXPONENT_COLLATOR - Integer.parseInt(exponent);
				result = "1 " + exponentValue + " "+ formattedSignificand;
			} else {                 
				result = "2 " + exponent.substring(1) + " " + formattedSignificand;
			}
		}  else {
			if (d == 0.0D) {      
				result = "3 000 0.0000000000000000";
			} else if (negativeExponent) {  
				int exponentValue = Integer.parseInt(exponent) + EXPONENT_COLLATOR;
				result = "4 " + exponentValue + " " + significand;
			} else {                 
				result = "5 " + exponent + " " + significand;
			}
		}
		return result;
	}
	
	public static double decodeDouble(String s) {
		char caseNumber = s.charAt(0);
		if (caseNumber == '3') { return 0.0D; }
		String exponentString = s.substring(2, 5);
		int exponent = Integer.parseInt(exponentString);
		String significand = s.substring(6);
		
		if (caseNumber == '4') {
			exponent -= EXPONENT_COLLATOR;
		} else if (caseNumber == '1' || caseNumber == '2'){
			BigDecimal collatedSignificand = new BigDecimal(significand);
			BigDecimal significandValue = collatedSignificand.subtract(SIGNIFICAND_COLLATOR);
			significand = significandValue.toString();
			if (caseNumber == '1') {
				exponent = EXPONENT_COLLATOR - exponent;
			} else if (caseNumber == '2'){
				exponent = -exponent;
			}
		}
		return Double.parseDouble(significand + "E" + exponent);
	}
	
}
