/*
** 1998-08-31 -	This little module provides helper functions when dealing with representations
**		of numbers, specifically numbers describing the size of things. For example,
**		this module knows that 1457664 bytes can be expressed conveniently as 1.39 MB.
**		I haven't tried teaching it to make that into the "official" 1.44 MB yet, though...
** 1999-05-09 -	Added explicit support for 64-bit sizes. Might not be very portable. :(
*/

#include <ctype.h>
#include <stdio.h>
#include <string.h>

#include "strutil.h"
#include "sizeutil.h"

/* ----------------------------------------------------------------------------------------- */

struct UnitInfo {
	gchar	*name;
	gdouble	multiplier;
} unit_info[] = { {"bytes", 1.0}, {"GB", (double) (1 << 30)}, {"KB", (double) (1 << 10)},
		  {"MB", (double) (1 << 20)}, };

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-01 -	Perform one recursive step in a binary search. */
static gdouble find_multiplier(gchar *unit, guint low, guint high)
{
	guint	middle = low + (high - low) / 2, rel;

	if(high < low)
		return -1.0f;
	if((rel = strcmp(unit, unit_info[middle].name)) == 0)
		return unit_info[middle].multiplier;
	else if(rel < 0)
		return find_multiplier(unit, low, middle - 1);
	return find_multiplier(unit, middle + 1, high);
}

/* 1998-09-01 -	Initialize a binary search for the multiplier associated with the given
**		unit name.
*/
static gdouble get_multiplier(gchar *unit)
{
	return find_multiplier(unit, 0U, sizeof unit_info / sizeof unit_info[0] - 1);
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-08-31 -	Attempt to parse a size from the string beginning at <buf>. Parsing rules:
**		* Initial whitespace is ignored.
**		* The first thing that can be parsed as a positive number will be. Floating
**		  point notation is (somewhat) understood.
**		* After the number, a unit can appear, optionally prefixed by whitespace.
**		  Legal units are: bytes, KB (1024 bytes), MB (1024^2 bytes) and GB (1024^3).
**		  If no unit appears, bytes (multiplier=1) is assumed. Unknown units are ignored.
*/
gsize sze_get_size(const gchar *buf)
{
	gchar	unit[16];
	gdouble	ipart = 0.0f, fpart = 0.0f, weight = 0.1f, num, mult = 1.0f;

	while(isspace((int) *buf))
		buf++;

	if(isdigit((int) *buf))
	{
		while(isdigit((int) *buf))
		{
			ipart *= 10.0f;
			ipart += *buf - '0';
			buf++;
		}
		if(*buf == '.')
		{
			for(buf++; isdigit((int) *buf); buf++, weight /= 10.0f)
				fpart += (*buf - '0') * weight;
		}
		num = ipart + fpart;
		while(isspace((int) *buf))
			buf++;
		if(sscanf(buf, "%8s", unit) == 1)
		{
			if((mult = get_multiplier(unit)) < 0)
				mult = 1.0f;
		}
		return (gsize) mult * num;
	}
	return (gsize) -1;
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-01 -	Output the given <size> as a string in <buf>, using no more than <buf_max>
**		bytes to do so. The <unit> controls which unit will be appended to the number.
**		It should be one of:
**		SZE_NONE	No unit is appended. Function degenerates into snprintf().
**		SZE_BYTES	The unit will be "bytes".
**		SZE_KB		Unit is "KB"; divides the size down by 1024. 4711 => "4.60 KB".
**		SZE_MB		Megabytes, divisor is 1024 squared.
**		SZE_GB		Gigabytes, using a divisor of 1024 cubed.
**		SZE_AUTO	Automatic unit selection; uses heuristics to determine which
**				unit works "best". Avoids "0.87 GB", digs "890.08 MB".
**		This function returns the number of characters written to <buf>, not including
**		the terminating '\0'-character.
*/
guint sze_put_size(gsize size, gchar *buf, gsize buf_max, SzUnit unit)
{
	const gchar	*fmt = NULL;
	gdouble		temp;

	switch(unit)
	{
		case SZE_NONE:
			return g_snprintf(buf, buf_max, "%u", size);
		case SZE_BYTES:
			return g_snprintf(buf, buf_max, "%u bytes", size);
		case SZE_KB:
			if((size & ((1 << 10) - 1)) == 0)
				return g_snprintf(buf, buf_max, "%u KB", size >> 10);
			temp = (double) size / (1 << 10);
			fmt  = "%.2f KB";
			break;
		case SZE_MB:
			if((size & ((1 << 20) - 1)) == 0)
				return g_snprintf(buf, buf_max, "%u MB", size >> 20);
			temp = (double) size / (1 << 20);
			fmt  = "%.2f MB";
			break;
		case SZE_GB:
			if((size & ((1 << 30) - 1)) == 0)
				return g_snprintf(buf, buf_max, "%u GB", size >> 30);
			temp = (double) size / (1 << 30);
			fmt  = "%.2f GB";
			break;
		case SZE_AUTO:
			if(size < (1 << 10))
				return sze_put_size(size, buf, buf_max, SZE_BYTES);
			else if(size < (1 << 20))
				return sze_put_size(size, buf, buf_max, SZE_KB);
			else if(size < (1 << 30))
				return sze_put_size(size, buf, buf_max, SZE_MB);
			else
				return sze_put_size(size, buf, buf_max, SZE_GB);
	}
	if(fmt != NULL)
		return g_snprintf(buf, buf_max, fmt, temp);
	return 0U;
}

/* 1999-05-09 -	A version of the above that deals explicitly with 64-bit sizes. Handy
**		for the total selection, file system free bytes, and stuff.
** BUG BUG BUG	Because there is no standard %-formatting character for 64-bit output,
**		this might, again, not be very portable. On systems using a GNU C library
**		(or a compatible), it should work. I really don't feel like writing the
**		relevant code myself, at this point.
*/
guint sze_put_size64(guint64 size, gchar *buf, gsize buf_max, SzUnit unit)
{
	const gchar	*fmt = NULL;
	gdouble		temp;

	switch(unit)
	{
		case SZE_NONE:
			return g_snprintf(buf, buf_max, "%Lu", size);
		case SZE_BYTES:
			return g_snprintf(buf, buf_max, "%Lu bytes", size);
		case SZE_KB:
			if((size & ((1ULL << 10) - 1)) == 0)
				return g_snprintf(buf, buf_max, "%Lu KB", size >> 10);
			temp = (gdouble) size / (1ULL << 10);
			fmt  = "%.2f KB";
			break;
		case SZE_MB:
			if((size & ((1ULL << 20) - 1)) == 0)
				return g_snprintf(buf, buf_max, "%Lu MB", size >> 20);
			temp = (gdouble) size / (1ULL << 20);
			fmt  = "%.2f MB";
			break;
		case SZE_GB:
			if((size & ((1ULL << 30) - 1)) == 0)
				return g_snprintf(buf, buf_max, "%Lu GB", size >> 30);
			temp = (gdouble) size / (1ULL << 30);
			fmt  = "%.2f GB";
			break;
		case SZE_AUTO:
			if(size < (1ULL << 10))
				return sze_put_size64(size, buf, buf_max, SZE_BYTES);
			else if(size < (1ULL << 20))
				return sze_put_size64(size, buf, buf_max, SZE_KB);
			else if(size < (1ULL << 30))
				return sze_put_size64(size, buf, buf_max, SZE_MB);
			else
				return sze_put_size64(size, buf, buf_max, SZE_GB);
	}
	if(fmt != NULL)
		return g_snprintf(buf, buf_max, fmt, temp);
	return 0U;
}
