### ----------------------------------------------------
### -- Intl::Format::Number
### -- Licenses: Artistic-2.0
### -- Authors: Matthew Stephen Stuckwisch
### -- File: lib/Intl/Format/Number.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 3
use v6.d;
use Intl::LanguageTag:auth<zef:guifa>:ver<0.12.3+>;
use User::Language:auth<zef:guifa>:ver<0.5.1+>;
use experimental :rakuast;

subset NumberFormatLength of Str where 'standard' | 'short' | 'long';
subset NumberFormatType   of Str where 'decimal'  | 'percent' | 'permille' | 'scientific' | 'engineering';

#| Formats a number appropriately for the language
multi sub format-number (
    Numeric()           $number,                          #= The number that will be formatted
    LanguageTag()      :$language = INIT {user-language}, #= The language to use
    NumberFormatLength :$length   = 'standard',           #= Formatting length, options include standard, short, and long
    NumberFormatType   :$type     = 'decimal',            #= The number style to use (decimal, percent, scientific, currency)
) is export {
    state %cache;
    my $code := $language ~ $type ~ $length;
    my $formatter := %cache{$code};
    return $formatter($number) if $formatter;

    # The code wasn't cached so we have to prepare data and generate it
    use Intl::CLDR:auth<zef:guifa>:ver<0.7.4+>;
    use Intl::Format::Number::Grammar;
    my \numbers := cldr{$language}.numbers;
    my \num-sys := $language.extensions<u><nu>                                          #= The number system being used
                || numbers.numbering-systems.default;
    my \symbols := numbers.symbols{num-sys};                                            #= Symbols data, in hash-y format
    my \pattern := $type ne 'permille'                                                  #= Pattern data pre-parsing
                ?? numbers{$type ~ '-formats'}{num-sys}{$length}.pattern
                !! numbers<percent-formats>{num-sys}{$length}.pattern.subst('%','‰');
    my \format  := Grammar.parse(pattern).made;                                         #= Pattern data post-parsing

    use MONKEY-SEE-NO-EVAL; # RakuAST is by definition safe
    $formatter := EVAL format-number-rakuast(format, $type, num-sys, symbols);
    %cache{$code} := $formatter;
    return $formatter($number);
}

sub local-number-formatter(
    LanguageTag()      :$language = INIT {user-language}, #= The language to use
    NumberFormatLength :$length   = 'standard',           #= Formatting length, options include standard, short, and long
    NumberFormatType   :$type     = 'decimal',            #= The number style to use (decimal, percent, scientific, currency)
    *%options,
) is export {
    use Intl::CLDR:auth<zef:guifa>:ver<0.7.4+>;
    use Intl::Format::Number::Grammar;
    my \numbers := cldr{$language}.numbers;
    my \num-sys := $language.extensions<u><nu>                                          #= The number system being used
                || numbers.numbering-systems.default;
    my \symbols := numbers.symbols{num-sys};                                            #= Symbols data, in hash-y format
    my \pattern := $type ne 'permille'                                                  #= Pattern data pre-parsing
                ?? numbers{$type ~ '-formats'}{num-sys}{$length}.pattern
                !! numbers<percent-formats>{num-sys}{$length}.pattern.subst('%','‰');
    my \format  := Grammar.parse(pattern).made;                                         #= Pattern data post-parsing
    handle-options(format,%options);
    use MONKEY-SEE-NO-EVAL; # RakuAST is safe
    return EVAL format-number-rakuast(format, $type, num-sys, symbols)
}

#| Obtains a number formatter, optionally as a RakuAST sub
sub number-formatter($pattern, :$rast = False, *%options ) is export {
    use Intl::Format::Number::Grammar;
    my %format = Grammar.parse($pattern).made;
    handle-options(%format, %options); #= Pattern data post-parsing
    my $node := format-number-rakuast(%format, %format<type>, %format<number-system>, %format<symbols>);
    use MONKEY-SEE-NO-EVAL; # we are safe to use this here
    return $rast ?? $node !! (EVAL $node);
}


# Generate the following using the following (from cldr supplement)
# "numberingSystems.xml".IO.lines.grep(/numeric/)
#     .map(*.match: /'id="'(<.alpha>+).*?'digits="'['&#x'(<[0..9A..F]>+)|(.)]/)
#     .map({':'~.[0]~'('~(.[1].substr(0,1) eq <123456789ABCDEF>.comb.any ?? .[1].Str.parse-base(16) !! .[1].ord)~'), '})
#| The offset for the 0 digit for a number system
constant %digit-offset =  :adlm(125264),    :ahom(71472),     :arab(1632),     :arabext(1776),    :bali(6992),
        :beng(2534),      :bhks(72784),     :brah(69734),     :cakm(69942),    :cham(43600),      :deva(2406),
        :diak(72016),     :fullwide(65296), :gong(73120),     :gonm(73040),    :gujr(2790),       :guru(2662),
        :hanidec(12295),  :hmng(93008),     :hmnp(123200),    :java(43472),    :kali(43264),      :kawi(73552),
        :khmr(6112),      :knda(3302),      :lana(6784),      :lanatham(6800), :laoo(3792),       :latn(48),
        :lepc(7232),      :limb(6470),      :mathbold(120782),:mathdbl(120792),:mathmono(120822), :mathsanb(120812),
        :mathsans(120802),:mlym(3430),      :modi(71248),     :mong(6160),     :mroo(92768),      :mtei(44016),
        :mymr(4160),      :mymrshan(4240),  :mymrtlng(43504), :nagm(124144),   :newa(70736),      :nkoo(1984),
        :olck(7248),      :orya(2918),      :osma(66720),     :rohg(68912),    :saur(43216),      :segment(130032),
        :shrd(70096),     :sind(70384),     :sinh(3558),      :sora(69872),    :sund(7088),       :takr(71360),
        :talu(6608),      :tamldec(3046),   :telu(3174),      :thai(3664),     :tibt(3872),       :tirh(70864),
        :tnsa(92864),     :vaii(42528),     :wara(71904),     :wcho(123632);

#| Sanity checks the formatting options,
sub handle-options(%format is raw, %options) {
    for %options.kv -> $key, $value {
        given $key {
            when 'maximum-integer-digits'     | 'max-int-digs'  { %format<maximum-integer-digits>     = $value      }
            when 'minimum-integer-digits'     | 'min-int-digs'  { %format<minimum-integer-digits>     = $value      }
            when 'maximum-fractional-digits'  | 'max-frac-digs' { %format<maximum-fractional-digits>  = $value      }
            when 'minimum-fractional-digits'  | 'min-frac-digs' { %format<minimum-fractional-digits>  = $value      }
            when 'maximum-significant-digits' | 'max-sig-digs'  { %format<maximum-significant-digits> = $value      }
            when 'minimum-significant-digits' | 'min-sig-digs'  { %format<minimum-significant-digits> = $value      }
            when 'minimum-exponential-digits' | 'min-exp-digs'  { %format<minimum-exponential-digits> = $value      }
            when 'exponential-power-multiple'                   { %format<exponential-power-multiple> = $value      }
            when 'type'                                         { %format<type>                       = $value      }
            when 'digits'                                       { %format<digits>                     = $value.list }
        }
    }

    # Sanity checks
    with %format {
        # Bad values
        if .<primary-grouping-size> < 0 {
            warn "Primary grouping size ({.<primary-grouping-size>}) cannot be less than zero.\n"
               ~ "Setting it to 0.";
           .<primary-grouping-size> = 0
        }
        if .<secondary-grouping-size> < 0 {
            warn "Secondary grouping size ({.<secondary-grouping-size>}) cannot be less than zero.\n"
               ~ "Setting it to 0.";
           .<secondary-grouping-size> = 0
        }
        if .<fractional-grouping-size> < 0 {
            warn "Fractional grouping size ({.<fractional-grouping-size>}) cannot be less than zero.\n"
               ~ "Setting it to 0.";
           .<fractional-grouping-size> = 0
        }
        if .<minimum-exponential-digits> < 0 {
            warn "Minimum exponential digits ({.<minimum-exponential-digits>}) cannot be less than zero.\n"
               ~ "Setting it to 0.";
           .<minimum-exponential-digits> = 0
        }
        if .<exponential-power-multiple> < 0 {
            warn "Exponential power multiple ({.<exponential-power-multiple>}) cannot be less than zero.\n"
               ~ "Setting it to 0.";
           .<exponential-power-multiple> = 0
        }


        # Incompatible values
        if .<maximum-integer-digits> < .<minimum-integer-digits> {
            warn "Maximum integer digits ({.<maximum-integer-digits>}) cannot be less than minimum integer digits ({.<minimum-integer-digits>}).\n"
               ~ "Setting them both to {.<minimum-integer-digits> ≥ 0 ?? .<minimum-integer-digits> !! '0 because they cannot be negative.'}";
            .<maximum-integer-digits> = .<minimum-integer-digits> max 0;
        }
        if .<maximum-fractional-digits> < .<minimum-fractional-digits> max 0 {
            warn "Maximum fractional digits ({.<maximum-fractional-digits>}) cannot be less than minimum fractional digits ({.<minimum-fractional-digits>}).\n"
               ~ "Setting them both to {.<minimum-fractional-digits>}";
           .<maximum-fractional-digits> = .<minimum-fractional-digits>
        }
        if .<maximum-significant-digits> < .<minimum-significant-digits> max 0 {
            warn "Maximum significant digits ({.<maximum-significant-digits>}) cannot be less than minimum significant digits ({.<minimum-significant-digits>}).\n"
               ~ "Setting them both to {.<minimum-significant-digits>}";
           .<maximum-significant-digits> = .<minimum-significant-digits>
        }
        if .<type> ne any <decimal percent permille scientific engineering> {
            warn "Number format type ({.<type>}) must be any of 'decimal', 'percent' 'permille', or 'scientific'.\n"
               ~ "Setting it to 'decimal'";
           .<type> = 'decimal'
        }
        unless .<number-system>:exists {
            # no warn, this is expected
            .<number-system> = 'latn';
        }
        if .<number-system> !(elem) %digit-offset.keys {
            warn "Number system should be a valid CLDR identifier for digits.\n"
               ~ "Setting it to 'latn' (Latin / Western Arabic)";
            .<digits> = 'latn';
        }
        unless .<symbols>:exists {
            .<symbols> = %();
        }
        constant default-symbols = %(
            :decimal<.>, :group<,>,
            :exponential<E>, :superscripting-exponent<^>,
            :minus<->, :plus<+>,
            :nan<NaN>, :infinity<∞>,
            :percent<%>, :permille<‰>,
            :approximately<~>, :currency-decimal<,>,
            :currency-group<,>
        );
        for default-symbols.kv -> $key, $value {
            .<symbols>{$key} //= $value;
        }
    }
}



##########################
# HELPER SUBS AND VALUES #
################################################################
# The following subs and constants make life much easier when  #
# working with RakuAST. The constants make things look cleaner #
# and more performant, and the subs reduce indentation levels  #
# although they don't actually improve performance.            #
################################################################

#| Wraps a list in a block with required intermediate elements
sub block-blockoid-statement-list(*@items) is pure {
    RakuAST::Block.new(
        body => RakuAST::Blockoid.new(
            RakuAST::StatementList.new: |@items
        )
    )
}

#| Wraps a list in a blockoid with required intermediate elements
sub blockoid-statement-list(*@items) is pure {
    RakuAST::Blockoid.new(RakuAST::StatementList.new: |@items)
}

# These constants make life so much easier
constant NumEqual      = RakuAST::Infix.new('==');
constant ExactlyEqual  = RakuAST::Infix.new('===');
constant Or            = RakuAST::Infix.new('||');
constant And           = RakuAST::Infix.new('&&');
constant Assign        = RakuAST::Infix.new(':=');
constant Divide        = RakuAST::Infix.new('/');
constant IntDivide     = RakuAST::Infix.new('div');
constant Minus         = RakuAST::Infix.new('-');
constant Plus          = RakuAST::Infix.new('+');
constant Times         = RakuAST::Infix.new('*');
constant Greater       = RakuAST::Infix.new('>');
constant GreaterEqual  = RakuAST::Infix.new('>=');
constant LessThan      = RakuAST::Infix.new('<');
constant Divisible     = RakuAST::Infix.new('%%');
constant Mod           = RakuAST::Infix.new('%');
constant Power         = RakuAST::Infix.new('**');
constant Concat        = RakuAST::Infix.new('~');
constant Predecrement  = RakuAST::Prefix.new('--');
constant ZeroToN       = RakuAST::Prefix.new('^');
constant One           = RakuAST::IntLiteral.new(1);
constant Ten           = RakuAST::IntLiteral.new(10);
constant Zero          = RakuAST::IntLiteral.new(0);
constant Var_N         = RakuAST::Var::Lexical.new('$n');
constant Result        = RakuAST::Var::Lexical.new('$result');
#constant MinSigDigs    = RakuAST::Var::Lexical.new('$MIN-SIG-DIGS');
#constant LocalDigits   = RakuAST::Var::Lexical::Constant.new('digits');
#constant LatinDigits   = RakuAST::Var::Lexical::Constant.new('latn-digits');
constant Infinity      = RakuAST::NumLiteral.new(Inf);
constant NotNumber     = RakuAST::NumLiteral.new(NaN);
constant Tolerance     = RakuAST::Var::Dynamic.new('$*TOLERANCE');
constant CallLast      = RakuAST::Call::Name.new:   name => RakuAST::Name.from-identifier('last');
constant FloorMethod   = RakuAST::Call::Method.new: name => RakuAST::Name.from-identifier('floor');
constant CeilingMethod = RakuAST::Call::Method.new: name => RakuAST::Name.from-identifier('ceiling');
constant AbsMethod     = RakuAST::Call::Method.new: name => RakuAST::Name.from-identifier('abs');
constant Log10Method   = RakuAST::Call::Method.new: name => RakuAST::Name.from-identifier('log10');
constant MakeInt       = RakuAST::Call::Method.new: name => RakuAST::Name.from-identifier('Int');
constant MakeStr       = RakuAST::Call::Method.new: name => RakuAST::Name.from-identifier('Str');
constant RAST-ImplicitConstant = RakuAST::VarDeclaration::Implicit::Constant;
constant Var_X = RakuAST::Var::Lexical.new('$x');
constant Var_C = RakuAST::Var::Lexical.new('$c');
constant Var_R = RakuAST::Var::Lexical.new('$r');


use MONKEY-SEE-NO-EVAL;



sub format-number-rakuast(
    Associative \format,
    Str          $type,
    Str          $number-system,
    Associative \symbols        # must respond to specific methods
) is export(:ast) {
    use experimental :rakuast;

    # my Str $result = '';
    my $result-declare := RakuAST::Statement::Expression.new(
        expression => RakuAST::VarDeclaration::Simple.new( :scope<my>,:name<$result>,
            type  => RakuAST::Type::Simple.new(RakuAST::Name.from-identifier('Str')),
            initializer => RakuAST::Initializer::Assign.new(RakuAST::StrLiteral.new(''))
        )
    );

    my @statements =
        # Define the two subs that we need first,
        # if we need them (transliterate is a noop if 'latn')
        rast-magnitude-declare(),
        rast-transliterate($number-system, |(BEGIN %(($*RAKU.compiler.name eq 'rakudo' ?? 'nqp' !! 'ordhyper') => True ))),
        # Now declare our results object and the $n variable used for initial formatting
        $result-declare,
        rast-abs-n(),
        # Handle any adjustments to the value of $n and declare additional
        # variables potentially needed later (e.g. significant digits).
        # These are noops if not necessary
        rast-percent-adjust(format<type>),
        # Next format the number.  We shift this off to a different sub because
        # logic is pretty different based on various formatting options given.
        rast-number(format,symbols,$number-system),
        # Lastly, affix whatever prefix/suffix may apply.
        # In most cases, this will convert into noops.
        rast-fix(format, symbols, :type<prefix>),
        rast-fix(format, symbols, :type<suffix>);

    RakuAST::Sub.new(
        name => RakuAST::Name.from-identifier('format-number'),
        body => blockoid-statement-list(@statements),
        signature => RakuAST::Signature.new(
            parameters => (
                RakuAST::Parameter.new( target => RakuAST::ParameterTarget::Var.new('$number') ),
    )   )   )
}

sub get-number-formatter(|c) is export(:test) {
    use MONKEY-SEE-NO-EVAL;
    EVAL format-number-rakuast |c;
}

#| ([~] PREFIX/SUFFIX)
sub rast-fix(%data, \symbols, :$type!) {
    # There are two factors in generating the prefix/suffix.
    # (1) What is the negative style?
    #     (a) simple (negative and positive use the same)
    #     (b) unique (use different)
    # (2) Can merge all text that's not a - (assume a simple terminal - if there isn't one and it's simple)

    my $result;
    if %data<negative-type> eq 'simple' {
        # It's simple, so we use positive patterns always.
        # The only question is whether to automatically include a
        # negation symbol which is based on whether it's a prefix
        $result = rast-text(%data{"positive-$type"}, symbols, :auto-negative($type eq 'prefix'), :show-sign(%data<show-sign>));
    } else {
        # It's complex, so we use
        # ($number < 0 || $show-plus) ?? NEG !! POS
        # but optimize roughly
        my $negative := rast-text(%data{"negative-$type"}, symbols, :show-sign(%data<show-sign>));
        my $positive := rast-text(%data{"positive-$type"}, symbols, :show-sign(%data<show-sign>));

        # Both negative and positive patterns have text for this fix
        # TODO: handle $*show-sign logic
        if $negative || $positive {
            $result = RakuAST::Ternary.new(
                condition => RakuAST::Statement::Expression.new(
                        left => RakuAST::ApplyInfix.new(
                            left => RakuAST::Var::Lexical.new('$number'),
                            infix => LessThan,
                            right => RakuAST::IntLiteral(0)
                        ),
                ),
                then => ($negative // RakuAST::StrLiteral.new('')),
                else => ($positive // RakuAST::StrLiteral.new('')),
            )
        }
        # else { $result = Nil } (it should already be it)
    }



    # TODO: return empty if nothing here
    with $result {
        return RakuAST::Statement::Expression.new(
            expression => RakuAST::ApplyInfix.new(
                left => Result,
                infix => Assign,
                right => ($type eq 'prefix'
                    ?? RakuAST::ApplyInfix.new(:left($result),:infix(Concat),:right(Result ))
                    !! RakuAST::ApplyInfix.new(:left(Result ),:infix(Concat),:right($result))
                )
            )
        )
    } else {
        return Empty
    }
}

# (used by rast-fix to process pattern symbols, returns Nil if no patterns)
sub rast-text(@fix, \symbols, :$auto-negative = False, :$show-sign = False) {
    return Nil unless @fix || $auto-negative;

    my $standard-sign = True;
    my @strs;

    for @fix {
        FIRST my $str = '';
        LAST @strs.push(RakuAST::StrLiteral.new($str)) if $str;

        if .<type> eq 'text' {
            $str ~= .<text>
        } elsif .<symb> eq 'percent' {
            @strs.push: RakuAST::StrLiteral.new(symbols.percent)
        } elsif .<symb> eq 'permille' {
            @strs.push: RakuAST::StrLiteral.new(symbols.permille)
        } else #`[ .<symb> eq 'minus'] {
            # Push current string
            @strs.push: RakuAST::StrLiteral.new($str);
            @strs.push: rast-sign(symbols, $show-sign);

            # (re)set variables
            $str = '';
            $standard-sign = False;
        }
    }

    @strs.push: rast-sign(symbols, $show-sign) if $auto-negative && $standard-sign;

    # wrap with string joins, unless a single item
    if @strs > 1 {
        return RakuAST::ApplyListInfix.new(
            infix => Concat,
            operands => @strs.list
        );
    } else {
        return @strs.head
    }
}

#| Expression providing a minus sign for * < 0, and a plus for * ≥ 0 if $*show-sign
sub rast-sign(\symbols, $show-sign)  {
    # Equivalent to
    #     $number < 0 ?? '-' !! '';
    # Or, if $show-sign is True,
    #     $number < 0 ?? '-' !! '+'
    RakuAST::Ternary.new(
        condition => RakuAST::Statement::Expression.new(
            expression => RakuAST::ApplyInfix.new(
                left => RakuAST::Var::Lexical.new('$number'),
                infix => LessThan,
                right => Zero
            )
        ),
        then => RakuAST::StrLiteral.new(symbols<minus>),
        else => RakuAST::StrLiteral.new($show-sign ?? symbols<plus> !! ''),
    )
}

sub rast-number (\format, \symbols, \number-system) {
    my @number-statements;

    # The two different formatting systems require keeping up with a few different variables,
    # so it's actually a bit easier to break into two different methods.
    if format<minimum-significant-digits> == 0
    && format<type> ne 'scientific' | 'engineering' {
        # We are using max/min digits
        @number-statements = rast-min-max-digits(format,symbols, number-system)
    } else {
        # We are using significant digits (exponential or other specialized ones)
        @number-statements = rast-significant-digits(format,symbols,number-system);
    }

    # Package it up.  No matter what, the two special Num values are given
    # their own formatting style.  Otherwise, use the decimal formats.
    # TODO: add in RBNF for non-decimal formats.
    #
    # if    $n === NaN {   $result ~ 'NaN'   }
    # elsif $n === Inf {   $result ~  '∞'    }
    # else             { [number-statements] }
    RakuAST::Statement::If.new(
        condition => RakuAST::ApplyInfix.new( :left(Var_N), :infix(ExactlyEqual), :right(NotNumber)),
        then => block-blockoid-statement-list(
            RakuAST::Statement::Expression.new(
                expression => RakuAST::ApplyInfix.new( :left(Result), :infix(Assign),
                    right => RakuAST::ApplyInfix.new(:left(Result),:infix(Concat),:right(RakuAST::StrLiteral.new: symbols<nan>))
        )   ),  ),
        elsifs => [
            RakuAST::Statement::Elsif.new(
                condition => RakuAST::ApplyInfix.new(:left(Var_N), :infix(ExactlyEqual), :right(Infinity)),
                then => block-blockoid-statement-list(
                    RakuAST::Statement::Expression.new(
                        expression => RakuAST::ApplyInfix.new(:left(Result), :infix(Assign),
                            right => RakuAST::ApplyInfix.new(:left(Result),:infix(Concat),:right(RakuAST::StrLiteral.new: symbols<infinity>)),
        ),  ),  ),  ),  ],
        else => block-blockoid-statement-list(@number-statements)
    );
}


#| my $n = abs $number;
sub rast-abs-n is pure {
    RakuAST::Statement::Expression.new(
        expression => RakuAST::VarDeclaration::Simple.new(:scope<my>,:name<$n>,
            initializer => RakuAST::Initializer::Assign.new(
                RakuAST::Call::Name.new(
                    name => RakuAST::Name.from-identifier('abs'),
                    args => RakuAST::ArgList.new(RakuAST::Var::Lexical.new('$number'))
   )   )   )   )
}

#| $n = $n % (10 ** ($max - 1)) [empty if $max-int-digs is Inf]
sub rast-max-int(\format) {
    return Empty if format<maximum-integer-digits> === Inf | Any;
    RakuAST::Statement::Expression.new(
        expression => RakuAST::ApplyInfix.new(:left(Var_N), :infix(Assign),
            right => RakuAST::ApplyInfix.new(:left(Var_N) , :infix(Mod),
                right => RakuAST::ApplyInfix.new(:left(Ten), :infix(Power),
                    right => RakuAST::IntLiteral.new(format<maximum-integer-digits> - 1)
    )   )   )   )
}


sub rastlog($start, $var, $end = '') {
    RakuAST::Statement::Expression.new(
        expression => RakuAST::ApplyPostfix.new(
            operand => RakuAST::ApplyInfix.new(
                left => RakuAST::StrLiteral.new($start),
                infix => Concat,
                right => RakuAST::ApplyInfix.new(
                    right => RakuAST::StrLiteral.new($end),
                    infix => Concat,
                    left => RakuAST::Var::Lexical.new($var)
                )
            ),
            postfix => RakuAST::Call::Method.new(name => RakuAST::Name.from-identifier('say'))
        )
    )
}


sub rast-decimal(\symbols) {
    RakuAST::Statement::Expression.new(
        expression => RakuAST::ApplyInfix.new(
            left => Result,
            infix => Assign,
            right =>  RakuAST::ApplyInfix.new(
                left => Result,
                infix => Concat,
                right => RakuAST::StrLiteral.new(symbols.decimal)
            )
        ),
        condition-modifier => RakuAST::StatementModifier::If.new(
            RakuAST::Statement::Expression.new(
                expression => RakuAST::ApplyInfix.new(
                    right => RakuAST::Var::Lexical.new('$show-decimal'),
                    infix => Or,
                    left => RakuAST::ApplyInfix.new(
                        # TODO: handle rounding here potentially
                        left => Var_N,
                        infix => Greater,
                        right => RakuAST::Var::Lexical.new('$limit'),
                    )
                )
            )
        )
    )
}


sub rast-percent-adjust($type) {
    if $type eq 'percent' || $type eq 'permille' {
        return RakuAST::Statement::Expression.new(
            expression => RakuAST::ApplyInfix.new(
                left => Var_N,
                infix => Assign,
                right => RakuAST::ApplyInfix.new(
                    left => Var_N,
                    infix => Times,
                    right => RakuAST::IntLiteral.new($type eq 'percent' ?? 100 !! 1000)
                )
            )
        )
    }
    return Empty
}


#| Handles the exponential part of significant number formatting.
#| A series of expresions using $exponential-power and
sub rast-exponential(\format, \symbols, \number-system) {
    # For the exponential section, first format the exponential symbol.
    # Then add a sign if needed (power of 0 is no sign, negative always,
    # plus only if <exponential-forces-sign>, and format the abs value:
    # $result = $result ~ '-' if $exp-pow < 0;                                  # don't force sign
    # $result = $result ~ ($exp-pow == 0 ?? '' !! ($exp-pow < 0 ?? '-' !! '+)); # if forced
    # $exp-pow = abs $exp-pow;
    # $result = translit($result.Str)
    constant ExpPower = RakuAST::Var::Lexical.new('$exponential-power');
    RakuAST::Statement::Expression.new(
        expression => RakuAST::ApplyInfix.new( :left(Result), :infix(Assign),
            right => RakuAST::ApplyInfix.new( :left(Result), :infix(Concat),
                right => RakuAST::StrLiteral.new(symbols<exponential>)
    )   )   ),
    (format<exponential-forces-sign>
        ?? RakuAST::Statement::Expression.new(
            expression => RakuAST::ApplyInfix.new( :left(Result), :infix(Assign),
                right => RakuAST::ApplyInfix.new( :left(Result), :infix(Concat),
                    right => RakuAST::Ternary.new(
                        condition => RakuAST::ApplyInfix.new( :left(ExpPower), :infix(NumEqual), :right(Zero) ),
                        then => RakuAST::StrLiteral.new(''),
                        else => RakuAST::Ternary.new(
                            condition => RakuAST::ApplyInfix.new( :left(ExpPower), :infix(LessThan), :right(Zero) ),
                            then => RakuAST::StrLiteral.new(symbols<minus>),
                            else => RakuAST::StrLiteral.new(symbols<plus>)
        )   )   )   )   )
        !! RakuAST::Statement::Expression.new(
            expression => RakuAST::ApplyInfix.new( :left(Result), :infix(Assign),
                right => RakuAST::ApplyInfix.new( left => Result, infix => Concat,
                    right => RakuAST::StrLiteral.new(symbols<minus>)
            )   ),
            condition-modifier => RakuAST::StatementModifier::If.new(
                RakuAST::ApplyInfix.new( :left(ExpPower), :infix(LessThan), :right(Zero),
    ))  )   ),
    RakuAST::Statement::Expression.new(expression =>
        RakuAST::ApplyInfix.new(
            left => ExpPower,
            infix => Assign,
            right => RakuAST::Call::Name.new(
                name => RakuAST::Name.from-identifier('abs'),
                args => RakuAST::ArgList.new(ExpPower),
    )   )   ),
    RakuAST::Statement::Expression.new(expression =>
        RakuAST::ApplyInfix.new( :left(Result), :infix(Assign),
            right => RakuAST::ApplyInfix.new( :left(Result), :infix(Concat),
                right => rast-transliterate-wrap(
                    RakuAST::ApplyPostfix.new( postfix => MakeStr, operand => ExpPower),
                    number-system
    )   )   )   ),
}

sub rast-transliterate-wrap(\node, \number-system) {
    number-system eq 'latn'
        ?? node
        !! RakuAST::Call::Name.new(
            name => RakuAST::Name.from-identifier('transliterate'),
            args => RakuAST::ArgList.new(node)
        )
}

sub rast-significant-digits(\format, \symbols, \number-system) {
    # Based on magnitude, calculate the minimum number of digits and format
    # as far as we need to go.  This will look VERY similar to min/max formatting
    # except that we assume infinite digits on each side

    # SETUP
    # $power = $n == 0 ?? 1 !! $n.log10;
    # $n = $n.round(10 ** ($power - $max-significant-digits));
    #
    # INTEGER DIGITS
    # $result = $result ~ ($power > 0 ?? $n.floor.Str !! '0');
    #
    # FRACTION
    # $n = $n - $n.floor;
    # my $completed-digits = $power max 0;
    # my $grouping-location = $completed-digits - $primary-grouping;

    my $needs-grouper = so format<primary-grouping-size>;
    my \MaxSigDigs = format<maximum-significant-digits> === Inf
        ?? RakuAST::Var::Lexical.new('$local-max-sig-digs')
        !! RakuAST::IntLiteral.new(format<maximum-significant-digits>);

    #| $n = $n.round(10 ** ($power - $max-significant-digits));
    #| # (cuts off extra digits)
    my @initial-n-adjust := rast-round(
        RakuAST::ApplyInfix.new( :left(Ten), :infix(Power),
            right => RakuAST::