import java.lang {
    JVMMath=Math
}

"The string decimal representation of the given 
 [[floating point number|float]]. If the given number is 
 [[negative|Float.negative]], the string representation will 
 begin with `-`. The [[whole part|Float.wholePart]] and 
 [[fractional parts|Float.fractionalPart]] of the number are
 separated by a `.` decimal point. Digits consist of decimal 
 digits `0` to `9`. 
 
 The number of decimal places following the decimal point is 
 controlled by the parameters [[minDecimalPlaces]] and 
 [[maxDecimalPlaces]], which default to `1` and `9` 
 respectively, so that by default the string representation
 always contains a decimal point, and never contains more 
 than nine decimal places. The decimal representation is 
 rounded so that the number of decimal places never
 exceeds the specified maximum.
 
 For example:
 
 - `formatFloat(1234.1234)` is `\"1234.1234\"`
 - `formatFloat(0.1234)` is `\"0.1234\"`
 - `formatFloat(1234.0)` is `\"1234.0\"`
 - `formatFloat(1234.0,0)` is `\"1234\"`
 - `formatFloat(1234.1234,6)` is `\"1234.123400\"`
 - `formatFloat(1234.1234,0,2)` is `\"1234.12\"`
 - `formatFloat(1234.123456,0,5)` is `\"1234.12346\"`
 - `formatFloat(0.0001,2,2)` is `\"0.00\"`
 - `formatFloat(0.0001,0,2)` is `\"0\"`
 
 Finally:
 
 - `formatFloat(-0.0)` is `\"0.0\"`,
 - `formatFloat(0.0/0)` is `\"NaN\"`,
 - `formatFloat(1.0/0)` is `\"Infinity\"`, and
 - `formatFloat(-1.0/0)` is `\"-Infinity\".`
 
 This function never produces a representation involving 
 scientific notation."
tagged("Numbers")
see (`function formatInteger`,
     `function parseFloat`)
since("1.2.0")
shared String formatFloat(
        "The floating point value to format."
        Float float,
        "The minimum number of allowed decimal places.

         If `minDecimalPlaces<=0`, the result may have no
         decimal point."
        variable Integer minDecimalPlaces=1,
        "The maximum number of allowed decimal places.

         If `maxDecimalPlaces<=0`, the result always has no
         decimal point."
        variable Integer maxDecimalPlaces=9,
        "The character to use as the decimal separator.

         `decimalSeparator` may not be '-' or a digit as
         defined by the Unicode general category *Nd*."
        Character decimalSeparator = '.',
        "If not `null`, `thousandsSeparator` will be used to
         separate each group of three digits, starting
         immediately to the left of the decimal separator.

         `thousandsSeparator` may not be equal to the
         decimalSeparator and may not be '-' or a digit as
         defined by the Unicode general category *Nd*."
        Character? thousandsSeparator = null) {

    if (exists thousandsSeparator) {
        "thousandsSeparator may not be '-' or a numeric digit."
        assert (!thousandsSeparator.digit
                && !thousandsSeparator == '-');

        "The same character may not be used for both
         thousandsSeparator and decimalSeparator."
        assert (thousandsSeparator != decimalSeparator);
    }

    "The decimalSeparator may not be '-' or a numeric digit."
    assert (!decimalSeparator.digit
            && !decimalSeparator == '-');

    // let's not be rude and throw
    if (maxDecimalPlaces < 0) {
        maxDecimalPlaces = 0;
    }
    if (minDecimalPlaces < 0) {
        minDecimalPlaces = 0;
    }
    if (maxDecimalPlaces < minDecimalPlaces) {
        maxDecimalPlaces = minDecimalPlaces;
    }

    // handle 0, undefined, and infinities
    if (float == 0) {
        if (minDecimalPlaces > 0) {
            return "0``decimalSeparator````"0".repeat(minDecimalPlaces)``";
        }
        return "0";
    }
    else if (float.undefined || float.infinite) {
        return float.string;
    }

    variable value wholeDigitNumber = 0;
    value thousands = thousandsSeparator?.string else "";

    value result = StringBuilder();
    value magnitude = float.magnitude;
    value decimalMoveRight = smallest {
        // Don't include more fractional digits than
        // necessary. See rounding (halfEven) below.
        maxDecimalPlaces;
        14 - exponent(magnitude);
    };

    "The float, but with all meaningful digits shifted to the
     first ~15 positions of the whole part"
    value normalized = scaleByPowerOfTen(magnitude, decimalMoveRight);

    "The usable digits: [[normalized]] as an [[Integer]] after rounding"
    variable value integer = halfEven(normalized).integer;

    "The number of digits of [[integer]] that are to the right of
     the decimal point in [[float]]. May be negative."
    variable value fractionalPartDigits = decimalMoveRight;

    "Have any digits to the right of the '.' been emitted?"
    variable value emittedFractional = false;

    if (minDecimalPlaces > fractionalPartDigits) {
        // we have fewer fractional digits than we need
        if (fractionalPartDigits > 0) {
            result.append("0".repeat(
                minDecimalPlaces - fractionalPartDigits));
        } else {
            result.append("0".repeat(minDecimalPlaces));
        }
        emittedFractional = true;
    }
    while (fractionalPartDigits > maxDecimalPlaces) {
        // we have more fractional digits than we need
        integer /= 10;
        fractionalPartDigits--;
    }
    while (fractionalPartDigits > 0) {
        // emit fractional part of 'integer'
        value digit = integer % 10;
        integer /= 10;
        if (digit != 0 || emittedFractional
                || fractionalPartDigits <= minDecimalPlaces) {
            result.appendCharacter('0'.neighbour(digit));
            emittedFractional = true;
        }
        fractionalPartDigits--;
    }
    if (emittedFractional) {
        result.appendCharacter(decimalSeparator);
    }
    if (integer == 0) {
        result.appendCharacter('0');
    }
    else {
        while (fractionalPartDigits++ < 0) {
            // we have fewer whole part digits than we need
            if (3.divides(wholeDigitNumber++)
                    && wholeDigitNumber != 1) {
                result.append(thousands);
            }
            result.appendCharacter('0');
        }
        while (integer != 0) {
            // emit whole part
            if (3.divides(wholeDigitNumber++)
                    && wholeDigitNumber != 1) {
                result.append(thousands);
            }
            value digit = integer % 10;
            integer /= 10;
            result.appendCharacter('0'.neighbour(digit));
        }
    }
    if (float < 0.0) {
        result.appendCharacter('-');
    }
    return(result.string.reversed);
}

Integer exponent(variable Float f)
    =>  let (l10 = log10(f.magnitude))
        // now, compute the floor
        if (l10.fractionalPart == 0.0 || l10 > 0.0)
        then l10.wholePart.integer
        else l10.wholePart.integer - 1;

Float scaleByPowerOfTen(Float float, variable Integer power) {
    function doScale(Float float, Integer power) {
        value scale =
                let (magnitude = power.magnitude)
                if (magnitude <= 15)
                    then (10^magnitude).nearestFloat     //fast
                    else 10.0.powerOfInteger(magnitude); //slow
        return if (power < 0)
                then float / scale
                else float * scale;
    }
    // don't attempt to create a float larger than 1.0e308.
    variable value result = float;
    while (power > 0) {
        value amount = smallest(308, power);
        result = doScale(result, amount);
        power -= amount;
    }
    while (power < 0) {
        value amount = largest(-308, power);
        result = doScale(result, amount);
        power -= amount;
    }
    return result;
}

Float twoFiftyTwo = (2^52).float;

Float halfEven(Float num) {
    if (num.infinite ||
            num.undefined ||
            num.fractionalPart == 0.0) {
        return num;
    }

    variable value result = num.magnitude;
    if (result >= twoFiftyTwo) {
        return num;
    }

    // else, round
    result = (twoFiftyTwo + result) - twoFiftyTwo;
    return result * num.sign.float;
}

native
Float log10(Float num);

native("jvm")
Float log10(Float num)
    =>  JVMMath.log10(num);

native("js")
Float log10(Float num) {
    dynamic {
        Float n = Math.log(num);
        Float d = Math.\iLN10;
        return n / d;
    }
}