/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */
package org.apache.jackrabbit.core.query.lucene;

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

/**
 * The <code>DecimalField</code> class is a utility to convert
 * <code>java.math.BigDecimal</code> values to <code>String</code> 
 * values that are lexicographically sortable according to the decimal value.
 * <p>
 * The string format uses the characters '0' to '9' and consists of:
 * <pre>
 * { value signum +2 }
 * { exponent signum +2 }
 * { exponent length -1 }
 * { exponent value }
 * { value (-1 if inverted) }
 * </pre>
 * Only the signum is encoded if the value is zero. The exponent is not
 * encoded if zero. Negative values are "inverted" character by character
 * ('0' -> 9, '1' -> '8', and so on). The same applies to the exponent.
 * <p>
 * Examples: 
 * 0 => "2"
 * 2 => "322" (signum 1; exponent 0; value 2)
 * 120 => "330212" (signum 1; exponent signum 1, length 1, value 2; value 12).
 * -1 => "179" (signum -1, rest inverted; exponent 0; value 1 (-1, inverted).
 * <p>
 * Values between BigDecimal(BigInteger.ONE, Integer.MIN_VALUE) and 
 * BigDecimal(BigInteger.ONE, Integer.MAX_VALUE) are supported.
 */
public class DecimalField {
    
    /**
     * Convert a BigDecimal to a String.
     * 
     * @param value the BigDecimal
     * @return the String
     */
    public static String decimalToString(BigDecimal value) {
        switch (value.signum()) {
        case -1:
            return "1" + invert(positiveDecimalToString(value.negate()), 1);
        case 0:
            return "2";
        default:
            return "3" + positiveDecimalToString(value);
        }
    }
    
    /**
     * Convert a String to a BigDecimal.
     * 
     * @param value the String
     * @return the BigDecimal
     */
    public static BigDecimal stringToDecimal(String value) {
        int sig = value.charAt(0) - '2';
        if (sig == 0) {
            return BigDecimal.ZERO;
        } else if (sig < 0) {
            value = invert(value, 1);
        }
        long expSig = value.charAt(1) - '2', exp;
        if (expSig == 0) {
            exp = 0;
            value = value.substring(2);
        } else {
            int expSize = value.charAt(2) - '0' + 1;
            if (expSig < 0) {
                expSize = 11 - expSize;
            }
            String e = value.substring(3, 3 + expSize);
            exp = expSig * Long.parseLong(expSig < 0 ? invert(e, 0) : e);
            value = value.substring(3 + expSize);
        }
        BigInteger x = new BigInteger(value);
        int scale = (int) (value.length() - exp - 1);
        return new BigDecimal(sig < 0 ? x.negate() : x, scale);
    }
    
    private static String positiveDecimalToString(BigDecimal value) {
        StringBuilder buff = new StringBuilder();
        long exp = value.precision() - value.scale() - 1;
        // exponent signum and size
        if (exp == 0) {
            buff.append('2');
        } else {
            String e = String.valueOf(Math.abs(exp));
            // exponent size is prepended
            e = String.valueOf(e.length() - 1) + e;
            // exponent signum
            if (exp > 0) {
                buff.append('3').append(e);
            } else {
                buff.append('1').append(invert(e, 0));
            }
        }
        String s = value.unscaledValue().toString();
        // remove trailing 0s
        int max = s.length() - 1;
        while (s.charAt(max) == '0') {
            max--;
        }
        return buff.append(s.substring(0, max + 1)).toString();
    }

    /**
     * "Invert" a number digit by digit (0 becomes 9, 9 becomes 0, and so on).
     * 
     * @param s the original string
     * @param incLast how much to increment the last character
     * @return the negated string
     */
    private static String invert(String s, int incLast) {
        char[] chars = s.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            chars[i] = (char) ('9' - chars[i] + '0');
        }
        chars[chars.length - 1] += incLast;
        return String.valueOf(chars);
    }

}
