use v5;

package Perlito5::JavaScript3::Sprintf;

sub emit_javascript3 {

    return <<'EOT';
/**
 * Copyright (c) 2010 Jakob Westhoff
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

CORE.sprintf = function(List__) {
    var format = List__[0];
    var list = List__[1];

    // Check for format definition
    if ( typeof format != 'string' ) {
        CORE.die(["sprintf: The first arguments need to be a valid format string."]);
    }
    
    /**
     * Define the regex to match a formating string
     * The regex consists of the following parts:
     * percent sign to indicate the start
     * (optional) sign specifier
     * (optional) padding specifier
     * (optional) alignment specifier
     * (optional) width specifier
     * (optional) precision specifier
     * type specifier:
     *  % - literal percent sign
     *  b - binary number
     *  c - ASCII character represented by the given value
     *  d - signed decimal number
     *  f - floating point value
     *  o - octal number
     *  s - string
     *  x - hexadecimal number (lowercase characters)
     *  X - hexadecimal number (uppercase characters)
     */
    var r = new RegExp( /%(\+)?([0 ]|'(.))?(-)?([0-9]+)?(\.([0-9]+))?([%bcdfosxX])/g );

    /**
     * Each format string is splitted into the following parts:
     * 0: Full format string
     * 1: sign specifier (+)
     * 2: padding specifier (0/<space>/'<any char>)
     * 3: if the padding character starts with a ' this will be the real 
     *    padding character
     * 4: alignment specifier
     * 5: width specifier
     * 6: precision specifier including the dot
     * 7: precision specifier without the dot
     * 8: type specifier
     */
    var parts      = [];
    var paramIndex = 0;
    while ( part = r.exec( format ) ) {
        // Check if an input value has been provided, for the current
        // format string
        if ( paramIndex >= list.length ) {
            CORE.die(["sprintf: At least one argument was missing."]);
        }

        parts[parts.length] = {
            /* beginning of the part in the string */
            begin: part.index,
            /* end of the part in the string */
            end: part.index + part[0].length,
            /* force sign */
            sign: ( part[1] == '+' ),
            /* is the given data negative */
            negative: ( parseInt( list[paramIndex] ) < 0 ) ? true : false,
            /* padding character (default: <space>) */
            padding: ( part[2] == undefined )
                     ? ( ' ' ) /* default */
                     : ( ( part[2].substring( 0, 1 ) == "'" ) 
                         ? ( part[3] ) /* use special char */
                         : ( part[2] ) /* use normal <space> or zero */
                       ),
            /* should the output be aligned left?*/
            alignLeft: ( part[4] == '-' ),
            /* width specifier (number or false) */
            width: ( part[5] != undefined ) ? part[5] : false,
            /* precision specifier (number or false) */
            precision: ( part[7] != undefined ) ? part[7] : false,
            /* type specifier */
            type: part[8],
            /* the given data associated with this part converted to a string */
            data: ( part[8] != '%' ) ? String ( list[paramIndex++] ) : false
        };
    }

    var newString = "";
    var start = 0;
    // Generate our new formated string
    for( var i=0; i<parts.length; ++i ) {
        // Add first unformated string part
        newString += format.substring( start, parts[i].begin );
        
        // Mark the new string start
        start = parts[i].end;

        // Create the appropriate preformat substitution
        // This substitution is only the correct type conversion. All the
        // different options and flags haven't been applied to it at this
        // point
        var preSubstitution = "";
        switch ( parts[i].type ) {
            case '%':
                preSubstitution = "%";
            break;
            case 'b':
                preSubstitution = Math.abs( parseInt( parts[i].data ) ).toString( 2 );
            break;
            case 'c':
                preSubstitution = String.fromCharCode( Math.abs( parseInt( parts[i].data ) ) );
            break;
            case 'd':
                preSubstitution = String( Math.abs( parseInt( parts[i].data ) ) );
            break;
            case 'f':
                preSubstitution = ( parts[i].precision == false )
                                  ? ( String( ( Math.abs( parseFloat( parts[i].data ) ) ) ) )
                                  : ( Math.abs( parseFloat( parts[i].data ) ).toFixed( parts[i].precision ) );
            break;
            case 'o':
                preSubstitution = Math.abs( parseInt( parts[i].data ) ).toString( 8 );
            break;
            case 's':
                preSubstitution = parts[i].data.substring( 0, parts[i].precision ? parts[i].precision : parts[i].data.length ); /* Cut if precision is defined */
            break;
            case 'x':
                preSubstitution = Math.abs( parseInt( parts[i].data ) ).toString( 16 ).toLowerCase();
            break;
            case 'X':
                preSubstitution = Math.abs( parseInt( parts[i].data ) ).toString( 16 ).toUpperCase();
            break;
            default:
                throw 'sprintf: Unknown type "' + parts[i].type + '" detected. This should never happen. Maybe the regex is wrong.';
        }

        // The % character is a special type and does not need further processing
        if ( parts[i].type ==  "%" ) {
            newString += preSubstitution;
            continue;
        }

        // Modify the preSubstitution by taking sign, padding and width
        // into account

        // Pad the string based on the given width
        if ( parts[i].width != false ) {
            // Padding needed?
            if ( parts[i].width > preSubstitution.length ) 
            {
                var origLength = preSubstitution.length;
                for( var j = 0; j < parts[i].width - origLength; ++j ) 
                {
                    preSubstitution = ( parts[i].alignLeft == true ) 
                                      ? ( preSubstitution + parts[i].padding )
                                      : ( parts[i].padding + preSubstitution );
                }
            }
        }

        // Add a sign symbol if neccessary or enforced, but only if we are
        // not handling a string
        if ( parts[i].type == 'b' 
          || parts[i].type == 'd' 
          || parts[i].type == 'o' 
          || parts[i].type == 'f' 
          || parts[i].type == 'x' 
          || parts[i].type == 'X' ) {
            if ( parts[i].negative == true ) {
                preSubstitution = "-" + preSubstitution;
            }
            else if ( parts[i].sign == true ) {
                preSubstitution = "+" + preSubstitution;
            }
        }

        // Add the substitution to the new string
        newString += preSubstitution;
    }

    // Add the last part of the given format string, which may still be there
    newString += format.substring( start, format.length );

    return newString;
};

CORE.printf = function(List__) {
    return CORE.print([ CORE.sprintf(List__) ]);
};

EOT
} # end of emit_javascript3()

1;

