/*
 * fmt.d
 *	A lightweight string formatter
 *
 * This offers a subset of sprintf(), for cases where the full-size
 * features (and code size) of the standard library formatter are
 * not appropriate.
 */
module fmt;

/*
 * isdig()
 *	Is @c a digit?
 */
bool
isdig(char c)
{
    return (c >= '0') && (c <= '9');
}

/*
 * ntoa()
 *	Return string for number
 */
string
ntoa(T)(T val)
{
    string res = "";

    // Negative number
    bool neg = false;
    if (val < 0) {
	val = -val;
	neg = true;
    }

    // Assemble digits
    do {
	char dig = '0' + (val % 10);
	res = dig ~ res;
	val /= 10;
    } while (val != 0);

    // Add negative sign?
    if (neg) {
	res = '-' ~ res;
    }

    return res;
}

/*
 * format_num()
 *	Format %d value
 */
private:
string format_num(T)(int count, bool zpad, T val)
{
    // Basic number string
    string res = ntoa(val);

    // Pad on right if negative count
    bool padr = false;
    if (count < 0) {
	count = -count;

	// It's not rational to pad zeroes here
	if (!zpad) {
	    padr = true;
	}
    }

    // Padding
    while (res.length < count) {
	if (padr) {
	    res = res ~ ' ';
	} else {
	    res = (zpad ? '0' : ' ') ~ res;
	}
    }

    return res;
}

/*
 * format_s()
 *	Format %s value
 */
string format_s(int count, in string val)
{
    string res = val.dup();
    bool padl = false;
    if (count < 0) {
	count = -count;
	padl = true;
    }
    while (res.length < count) {
	if (padl) {
	    res = ' ' ~ res;
	} else {
	    res ~= ' ';
	}
    }
    return res;
}

// Different types stored in an ArgPos
enum ap_type { AP_INT, AP_UINT, AP_LONG, AP_ULONG, AP_STR, AP_CHAR };

// The value in one single argument position
struct ArgPos {
    ap_type ap_typ;
    union {
	int ap_int;
	uint ap_uint;
	long ap_long;
	ulong ap_ulong;
	string ap_str;
	char ap_char;
    }
    this(in int val) {
	ap_typ = ap_type.AP_INT;
	ap_int = val;
    }
    this(in uint val) {
	ap_typ = ap_type.AP_UINT;
	ap_int = val;
    }
    this(in long val) {
	ap_typ = ap_type.AP_LONG;
	ap_long = val;
    }
    this(in ulong val) {
	ap_typ = ap_type.AP_ULONG;
	ap_long = val;
    }
    this(in string val) {
	ap_typ = ap_type.AP_STR;
	ap_str = val;
    }
    this(in char val) {
	ap_typ = ap_type.AP_CHAR;
	ap_char = val;
    }
    int as_int() {
	assert(ap_typ == ap_type.AP_INT);
	return(ap_int);
    }
    uint as_uint() {
	if (ap_typ == ap_type.AP_UINT) {
	    return(ap_uint);
	}
	assert(ap_typ == ap_type.AP_INT);
	assert(ap_int >= 0);
	return(ap_int);
    }
    long as_long() {
	assert(ap_typ == ap_type.AP_LONG);
	return(ap_long);
    }
    ulong as_ulong() {
	if (ap_typ == ap_type.AP_ULONG) {
	    return(ap_ulong);
	}
	assert(ap_typ == ap_type.AP_LONG);
	assert(ap_long >= 0);
	return(ap_long);
    }
    string as_str() {
	if (ap_typ == ap_type.AP_STR) {
	    return(ap_str);
	}
	assert(ap_typ == ap_type.AP_CHAR);
	return "" ~ ap_char;
    }
    char as_char() {
	assert(ap_typ == ap_type.AP_CHAR);
	return(ap_char);
    }
}

/*
 * fmt()
 *	Convert args and printf-style string to display string
 *
 * (This is my exploration of a smaller implementation than the
 * system one, which clocks in at ~200k of code.)
 *
 * Note that \n and friends are handled in string construction,
 * so we don't need to worry about that here.
 */
public:
string
fmt(T...)(in string fmts, T inargs)
{
    auto res = "";
    bool infmt, incount, zpad, isneg, modl;
    int count;
    size_t narg = 0;
    ArgPos[] args;

    // Pre-process our varargs
    foreach (arg; inargs) {
	args ~= ArgPos(arg);
    }

    infmt = incount = modl = false;
    foreach (c; fmts) {
	// Assembling count for formatting
	if (incount) {
	    if (isdig(c)) {
		count = (count * 10) + (c - '0');
		continue;
	    }

	    // Done with count
	    incount = false;
	    assert(infmt);
	}

	// In format?  Emit content for this position
	if (infmt) {
	    if (isdig(c)) {
		count = c - '0';
		if (c == '0') {
		    zpad = true;
		}
		isneg = false;
		incount = true;
		continue;
	    }
	    if (c == '-') {
		count = 0;
		isneg = incount = true;
		continue;
	    }

	    // Long modifier?
	    if (c == 'l') {
		modl = true;
		continue;
	    }

	    // Ready to format this argument
	    if (narg == args.length) {
		throw new Exception("Too few arguments to fmt()");
	    }
	    auto arg = args[narg++];
	    string s;

	    // Apply negation now
	    if (isneg) {
		count = -count;
	    }

	    // Dispatch format
	    switch (c) {
	    case 'd':
		if (modl) {
		    s = format_num(count, zpad, arg.as_long());
		} else {
		    s = format_num(count, zpad, arg.as_int());
		}
		break;
	    case 'u':
		if (modl) {
		    s = format_num(count, zpad, arg.as_ulong());
		} else {
		    s = format_num(count, zpad, arg.as_uint());
		}
		break;
	    case 's':
		s = format_s(count, arg.as_str());
		break;
	    case 'c':
		s = arg.as_str();
		break;
	    default:
		throw new Exception("Unsupported format char: " ~ c);
		break;
	    }

	    // Another format position processed
	    res ~= s;
	    infmt = false;
	    continue;
	}

	// Start formatting
	if (c == '%') {
	    infmt = true;
	    modl = isneg = zpad = false;
	    count = 0;
	    continue;
	}

	// Non-formatting, use literally
	res ~= c;
    }

    return res;
}
