/*

* extsprintf.js: extended POSIX-style sprintf
*/

var mod_assert = require('assert'); var mod_util = require('util');

/*

* Public interface
*/

exports.sprintf = jsSprintf; exports.printf = jsPrintf; exports.fprintf = jsFprintf;

/*

* Stripped down version of s[n]printf(3c).  We make a best effort to throw an
* exception when given a format string we don't understand, rather than
* ignoring it, so that we won't break existing programs if/when we go implement
* the rest of this.
*
* This implementation currently supports specifying
*      - field alignment ('-' flag),
*      - zero-pad ('0' flag)
*      - always show numeric sign ('+' flag),
*      - field width
*      - conversions for strings, decimal integers, and floats (numbers).
*      - argument size specifiers.  These are all accepted but ignored, since
*        Javascript has no notion of the physical size of an argument.
*
* Everything else is currently unsupported, most notably precision, unsigned
* numbers, non-decimal numbers, and characters.
*/

function jsSprintf(fmt) {

var regex = [
    '([^%]*)',                          /* normal text */
    '%',                                /* start of format */
    '([\'\\-+ #0]*?)',                  /* flags (optional) */
    '([1-9]\\d*)?',                     /* width (optional) */
    '(\\.([1-9]\\d*))?',                /* precision (optional) */
    '[lhjztL]*?',                       /* length mods (ignored) */
    '([diouxXfFeEgGaAcCsSp%jr])'        /* conversion */
].join('');

var re = new RegExp(regex);
var args = Array.prototype.slice.call(arguments, 1);
var flags, width, precision, conversion;
var left, pad, sign, arg, match;
var ret = '';
var argn = 1;

mod_assert.equal('string', typeof (fmt));

while ((match = re.exec(fmt)) !== null) {
        ret += match[1];
        fmt = fmt.substring(match[0].length);

        flags = match[2] || '';
        width = match[3] || 0;
        precision = match[4] || '';
        conversion = match[6];
        left = false;
        sign = false;
        pad = ' ';

        if (conversion == '%') {
                ret += '%';
                continue;
        }

        if (args.length === 0)
                throw (new Error('too few args to sprintf'));

        arg = args.shift();
        argn++;

        if (flags.match(/[\' #]/))
                throw (new Error(
                    'unsupported flags: ' + flags));

        if (precision.length > 0)
                throw (new Error(
                    'non-zero precision not supported'));

        if (flags.match(/-/))
                left = true;

        if (flags.match(/0/))
                pad = '0';

        if (flags.match(/\+/))
                sign = true;

        switch (conversion) {
        case 's':
                if (arg === undefined || arg === null)
                        throw (new Error('argument ' + argn +
                            ': attempted to print undefined or null ' +
                            'as a string'));
                ret += doPad(pad, width, left, arg.toString());
                break;

        case 'd':
                arg = Math.floor(arg);
                /*jsl:fallthru*/
        case 'f':
                sign = sign && arg > 0 ? '+' : '';
                ret += sign + doPad(pad, width, left,
                    arg.toString());
                break;

        case 'x':
                ret += doPad(pad, width, left, arg.toString(16));
                break;

        case 'j': /* non-standard */
                if (width === 0)
                        width = 10;
                ret += mod_util.inspect(arg, false, width);
                break;

        case 'r': /* non-standard */
                ret += dumpException(arg);
                break;

        default:
                throw (new Error('unsupported conversion: ' +
                    conversion));
        }
}

ret += fmt;
return (ret);

}

function jsPrintf() {

var args = Array.prototype.slice.call(arguments);
args.unshift(process.stdout);
jsFprintf.apply(null, args);

}

function jsFprintf(stream) {

var args = Array.prototype.slice.call(arguments, 1);
return (stream.write(jsSprintf.apply(this, args)));

}

function doPad(chr, width, left, str) {

var ret = str;

while (ret.length < width) {
        if (left)
                ret += chr;
        else
                ret = chr + ret;
}

return (ret);

}

/*

* This function dumps long stack traces for exceptions having a cause() method.
* See node-verror for an example.
*/

function dumpException(ex) {

var ret;

if (!(ex instanceof Error))
        throw (new Error(jsSprintf('invalid type for %%r: %j', ex)));

/* Note that V8 prepends "ex.stack" with ex.toString(). */
ret = 'EXCEPTION: ' + ex.constructor.name + ': ' + ex.stack;

if (ex.cause && typeof (ex.cause) === 'function') {
        var cex = ex.cause();
        if (cex) {
                ret += '\nCaused by: ' + dumpException(cex);
        }
}

return (ret);

}