package org.solovyev.common;

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

import javax.annotation.Nonnull;

import midpcalc.Real;

import static java.lang.Math.pow;
import static midpcalc.Real.NumberFormat.FSE_ENG;
import static midpcalc.Real.NumberFormat.FSE_FIX;
import static midpcalc.Real.NumberFormat.FSE_NONE;
import static midpcalc.Real.NumberFormat.FSE_SCI;

public class NumberFormatter {

    public static final char NO_GROUPING = 0;
    public static final int NO_ROUNDING = -1;
    public static final int DEFAULT_MAGNITUDE = 5;
    public static final int MIN_PRECISION = 1;
    public static final int MAX_PRECISION = 15;

    private final Real.NumberFormat numberFormat = new Real.NumberFormat();
    private final Real real = new Real();
    private int format = FSE_NONE;
    private int simpleFormatMagnitude = DEFAULT_MAGNITUDE;
    private int precision = MAX_PRECISION;
    private char groupingSeparator;

    public void useScientificFormat(int simpleFormatMagnitude) {
        this.format = FSE_SCI;
        this.simpleFormatMagnitude = simpleFormatMagnitude;
    }

    public void useEngineeringFormat(int simpleFormatMagnitude) {
        this.format = FSE_ENG;
        this.simpleFormatMagnitude = simpleFormatMagnitude;
    }

    public void useSimpleFormat() {
        this.format = FSE_NONE;
        this.simpleFormatMagnitude = DEFAULT_MAGNITUDE;
    }

    public void setPrecision(int precision) {
        if (precision == NO_ROUNDING) {
            this.precision = NO_ROUNDING;
            return;
        }
        this.precision = Math.max(MIN_PRECISION, Math.min(precision, MAX_PRECISION));
    }

    public void setGroupingSeparator(char groupingSeparator) {
        this.groupingSeparator = groupingSeparator;
    }

    @Nonnull
    public CharSequence format(double value) {
        return format(value, 10);
    }

    @Nonnull
    public CharSequence format(@Nonnull  BigInteger value) {
        return format(value, 10);
    }

    @Nonnull
    public CharSequence format(double value, int radix) {
        checkRadix(radix);
        double absValue = Math.abs(value);
        final boolean simpleFormat = useSimpleFormat(radix, absValue);

        int precision = getPrecision();
        if (simpleFormat) {
            precision += 1;
            final int newScale = Math.max(1, (int) (precision * Math.max(1, radix / 10f)) - 1);
            value = BigDecimal.valueOf(value).setScale(newScale, BigDecimal.ROUND_HALF_UP).doubleValue();
            absValue = Math.abs(value);
        }
        if (simpleFormat) {
            numberFormat.fse = FSE_FIX;
        } else if (format == FSE_NONE) {
            // originally, a simple format was requested but we have to use something more appropriate, f.e. scientific
            // format
            numberFormat.fse = FSE_SCI;
        } else {
            numberFormat.fse = format;
        }
        numberFormat.thousand = groupingSeparator;
        numberFormat.precision = precision;
        numberFormat.base = radix;
        numberFormat.maxwidth = simpleFormat ? 100 : 30;

        if (radix == 2 && value < 0) {
            return "-" + prepare(absValue);
        }
        return prepare(value);
    }

    private int getPrecision() {
        return precision == NO_ROUNDING ? MAX_PRECISION : precision;
    }

    @Nonnull
    public CharSequence format(@Nonnull BigInteger value, int radix) {
        checkRadix(radix);
        final BigInteger absValue = value.abs();
        final boolean simpleFormat = useSimpleFormat(radix, absValue);

        if (simpleFormat) {
            numberFormat.fse = FSE_FIX;
        } else if (format == FSE_NONE) {
            // originally, a simple format was requested but we have to use something more appropriate, f.e. scientific
            // format
            numberFormat.fse = FSE_SCI;
        } else {
            numberFormat.fse = format;
        }
        numberFormat.thousand = groupingSeparator;
        numberFormat.precision = Math.max(0, Math.min(precision, MAX_PRECISION));
        numberFormat.base = radix;
        numberFormat.maxwidth = simpleFormat ? 100 : 30;

        if (radix == 2 && value.compareTo(BigInteger.ZERO) < 0) {
            return "-" + prepare(absValue);
        }
        return prepare(value);
    }

    private void checkRadix(int radix) {
        if (radix != 2 && radix != 8 && radix != 10 && radix != 16) {
            throw new IllegalArgumentException("Unsupported radix: " + radix);
        }
    }

    private boolean useSimpleFormat(int radix, double absValue) {
        if (radix != 10) {
            return true;
        }
        if (format == FSE_NONE) {
            // simple format should be used only if rounding is on or if number is big enough
            final boolean round = precision != NO_ROUNDING;
            return round || absValue >= pow(10, -MAX_PRECISION);
        }
        if (pow(10, -simpleFormatMagnitude) <= absValue && absValue < pow(10, simpleFormatMagnitude)) {
            return true;
        }
        return false;
    }

    private boolean useSimpleFormat(int radix, @Nonnull BigInteger absValue) {
        if (radix != 10) {
            return true;
        }
        if (format == FSE_NONE) {
            return true;
        }
        if (absValue.compareTo(BigInteger.valueOf((long) pow(10, simpleFormatMagnitude))) < 0) {
            return true;
        }
        return false;
    }

    @Nonnull
    private CharSequence prepare(double value) {
        return stripZeros(realFormat(value)).replace('e', 'E');
    }

    @Nonnull
    private CharSequence prepare(@Nonnull  BigInteger value) {
        return stripZeros(realFormat(value)).replace('e', 'E');
    }

    @Nonnull
    private String realFormat(double value) {
        real.assign(Double.toString(value));
        return real.toString(numberFormat);
    }

    @Nonnull
    private String realFormat(@Nonnull  BigInteger value) {
        real.assign(value.toString());
        return real.toString(numberFormat);
    }

    @Nonnull
    private String stripZeros(@Nonnull String s) {
        int dot = -1;
        int firstNonZero = -1;
        for (int i = 0; i < s.length(); i++) {
            final char c = s.charAt(i);
            if (c != '0' && c != groupingSeparator && firstNonZero == -1) {
                firstNonZero = i;
            }
            if (c == '.') {
                dot = i;
                break;
            }
        }
        if (firstNonZero == -1) {
            // all zeros
            return "";
        }
        if (dot < 0) {
            // no dot - no trailing zeros
            return s.substring(firstNonZero);
        }
        if (firstNonZero == dot) {
            // one zero before dot must be kept
            firstNonZero--;
        }
        final int e = s.lastIndexOf('e');
        final int i = findLastNonZero(s, e);
        final int end = i == dot ? i : i + 1;
        return s.substring(firstNonZero, end) + getExponent(s, e);
    }

    @Nonnull
    private String getExponent(@Nonnull String s, int e) {
        String exponent = "";
        if (e > 0) {
            exponent = s.substring(e);
            if (exponent.length() == 2 && exponent.charAt(1) == '0') {
                exponent = "";
            }
        }
        return exponent;
    }

    private int findLastNonZero(@Nonnull String s, int e) {
        int i = e > 0 ? e - 1 : s.length() - 1;
        for (; i >= 0; i--) {
            if (s.charAt(i) != '0') {
                break;
            }
        }
        return i;
    }
}
