/*
 * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2021 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * stdio/vcbprintf.c
 * Formats text and outputs it via callback functions.
 */

#include <errno.h>
#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>

static size_t convert_integer(char* destination, uintmax_t value,
                              uintmax_t base, const char* digits)
{
	size_t result = 1;
	uintmax_t copy = value;
	while ( base <= copy )
		copy /= base, result++;
	for ( size_t i = result; i != 0; i-- )
	{
		destination[i-1] = digits[value % base];
		value /= base;
	}
	return result;
}

static size_t noop_callback(void* ctx, const char* str, size_t amount)
{
	(void) ctx;
	(void) str;
	return amount;
}

int vcbprintf(void* ctx,
              size_t (*callback)(void*, const char*, size_t),
              const char* format,
              va_list parameters)
{
	if ( !callback )
		callback = noop_callback;

	size_t written = 0;
	bool rejected_bad_specifier = false;

	while ( *format != '\0' )
	{
		if ( *format != '%' )
		{
			size_t amount;
		print_c:
			amount = 1;
			while ( format[amount] && format[amount] != '%' )
				amount++;
			if ( callback(ctx, format, amount) != amount )
				return -1;
			format += amount;
			written += amount;
			continue;
		}

		const char* format_begun_at = format;

		if ( *(++format) == '%' )
			goto print_c;

		if ( rejected_bad_specifier )
		{
		incomprehensible_conversion:
			rejected_bad_specifier = true;
		unsupported_conversion:
			format = format_begun_at;
			goto print_c;
		}

		bool alternate = false;
		bool zero_pad = false;
		bool field_width_is_negative = false;
		bool prepend_blank_if_positive = false;
		bool prepend_plus_if_positive = false;
		bool group_thousands = false;
		bool alternate_output_digits = false;

		while ( true )
		{
			switch ( *format++ )
			{
			case '#': alternate = true; continue;
			case '0': zero_pad = true; continue;
			case '-': field_width_is_negative = true; continue;
			case ' ': prepend_blank_if_positive = true; continue;
			case '+': prepend_plus_if_positive = true; continue;
			case '\'': group_thousands = true; continue;
			case 'I': alternate_output_digits = true; continue;
			default: format--; break;
			}
			break;
		}

		(void) group_thousands;
		(void) alternate_output_digits;

		int field_width = 0;
		if ( *format == '*' && (format++, true) )
			field_width = va_arg(parameters, int);
		else while ( '0' <= *format && *format <= '9' )
			field_width = 10 * field_width + *format++ - '0';
		if ( field_width_is_negative )
			field_width = -field_width;
		size_t abs_field_width = (size_t) abs(field_width);

		size_t precision = SIZE_MAX;
		if ( *format == '.' && (format++, true) )
		{
			if ( *format == '*' && (format++, true) )
			{
				int int_precision = va_arg(parameters, int);
				if ( 0 <= int_precision )
					precision = (size_t) int_precision;
			}
			else if ( *format == '-' && (format++, true) )
			{
				while ( '0' <= *format && *format <= '9' )
					format++;
			}
			else
			{
				precision = 0;
				while ( '0' <= *format && *format <= '9' )
					precision = 10 * precision + *format++ - '0';
			}
		}

		enum length
		{
			LENGTH_SHORT_SHORT,
			LENGTH_SHORT,
			LENGTH_DEFAULT,
			LENGTH_LONG,
			LENGTH_LONG_LONG,
			LENGTH_LONG_DOUBLE,
			LENGTH_INTMAX_T,
			LENGTH_SIZE_T,
			LENGTH_PTRDIFF_T,
		};

		struct length_modifer
		{
			const char* name;
			enum length length;
		};

		struct length_modifer length_modifiers[] =
		{
			{ "hh", LENGTH_SHORT_SHORT },
			{ "h", LENGTH_SHORT },
			{ "", LENGTH_DEFAULT },
			{ "l", LENGTH_LONG },
			{ "ll", LENGTH_LONG_LONG },
			{ "L", LENGTH_LONG_DOUBLE },
			{ "j", LENGTH_INTMAX_T },
			{ "z", LENGTH_SIZE_T },
			{ "t", LENGTH_PTRDIFF_T },
		};

		enum length length = LENGTH_DEFAULT;
		size_t length_length = 0;
		for ( size_t i = 0;
		      i < sizeof(length_modifiers) / sizeof(length_modifiers[0]);
		      i++ )
		{
			size_t name_length = strlen(length_modifiers[i].name);
			if ( name_length < length_length )
				continue;
			if ( strncmp(format, length_modifiers[i].name, name_length) != 0 )
				continue;
			length = length_modifiers[i].length;
			length_length = name_length;
		}

		format += length_length;

		if ( *format == 'd' || *format == 'i' || *format == 'o' ||
		     *format == 'u' || *format == 'x' || *format == 'X' ||
		     *format == 'p' )
		{
			char conversion = *format++;

			bool negative_value = false;
			uintmax_t value;
			if ( conversion == 'p' )
			{
				value = (uintptr_t) va_arg(parameters, void*);
				conversion = 'x';
				alternate = !alternate;
				prepend_blank_if_positive = false;
				prepend_plus_if_positive = false;
			}
			else if ( conversion == 'i' || conversion == 'd' )
			{
				intmax_t signed_value;
				if ( length == LENGTH_SHORT_SHORT )
					signed_value = va_arg(parameters, int);
				else if ( length == LENGTH_SHORT )
					signed_value = va_arg(parameters, int);
				else if ( length == LENGTH_DEFAULT )
					signed_value = va_arg(parameters, int);
				else if ( length == LENGTH_LONG )
					signed_value = va_arg(parameters, long);
				else if ( length == LENGTH_LONG_LONG )
					signed_value = va_arg(parameters, long long);
				else if ( length == LENGTH_INTMAX_T )
					signed_value = va_arg(parameters, intmax_t);
				else if ( length == LENGTH_SIZE_T )
					signed_value = va_arg(parameters, ssize_t);
				else if ( length == LENGTH_PTRDIFF_T )
					signed_value = va_arg(parameters, ptrdiff_t);
				else
					goto incomprehensible_conversion;
				value = (negative_value = signed_value < 0) ?
				         - (uintmax_t) signed_value : (uintmax_t) signed_value;
			}
			else
			{
				if ( length == LENGTH_SHORT_SHORT )
					value = va_arg(parameters, unsigned int);
				else if ( length == LENGTH_SHORT )
					value = va_arg(parameters, unsigned int);
				else if ( length == LENGTH_DEFAULT )
					value = va_arg(parameters, unsigned int);
				else if ( length == LENGTH_LONG )
					value = va_arg(parameters, unsigned long);
				else if ( length == LENGTH_LONG_LONG )
					value = va_arg(parameters, unsigned long long);
				else if ( length == LENGTH_INTMAX_T )
					value = va_arg(parameters, uintmax_t);
				else if ( length == LENGTH_SIZE_T )
					value = va_arg(parameters, size_t);
				else if ( length == LENGTH_PTRDIFF_T )
					value = (uintmax_t) va_arg(parameters, ptrdiff_t);
				else
					goto incomprehensible_conversion;
				prepend_blank_if_positive = false;
				prepend_plus_if_positive = false;
			}

			const char* digits = conversion == 'X' ? "0123456789ABCDEF" :
			                                         "0123456789abcdef";
			uintmax_t base = (conversion == 'x' || conversion == 'X') ? 16 :
			                 conversion == 'o' ? 8 : 10;
			char prefix[3];
			size_t prefix_length = 0;
			size_t prefix_digits_length = 0;
			if ( negative_value )
				prefix[prefix_length++] = '-';
			else if ( prepend_plus_if_positive )
				prefix[prefix_length++] = '+';
			else if ( prepend_blank_if_positive )
				prefix[prefix_length++] = ' ';
			if ( alternate && (conversion == 'x' || conversion == 'X') && value != 0 )
			{
				prefix[prefix_digits_length++, prefix_length++] = '0';
				prefix[prefix_digits_length++, prefix_length++] = conversion;
			}
			if ( alternate && conversion == 'o' && value != 0 )
				prefix[prefix_digits_length++, prefix_length++] = '0';

			char output[sizeof(uintmax_t) * 3];
			size_t output_length = convert_integer(output, value, base, digits);
			if ( !precision && output_length == 1 && output[0] == '0' )
			{
				output_length = 0;
				output[0] = '\0';
			}
			size_t output_length_with_precision =
				precision != SIZE_MAX && output_length < precision ?
				precision :
			    output_length;

			size_t digits_length = prefix_digits_length + output_length;
			size_t normal_length = prefix_length + output_length;
			size_t length_with_precision = prefix_length + output_length_with_precision;

			bool use_precision = precision != SIZE_MAX;
			bool use_zero_pad = zero_pad && 0 <= field_width && !use_precision;
			bool use_left_pad = !use_zero_pad && 0 <= field_width;
			bool use_right_pad = !use_zero_pad && field_width < 0;

			if ( use_left_pad )
			{
				for ( size_t i = length_with_precision; i < abs_field_width; i++ )
				{
					if ( callback(ctx, " ", 1) != 1 )
						return -1;
					written++;
				}
			}
			if ( callback(ctx, prefix, prefix_length) != prefix_length )
				return -1;
			written += prefix_length;
			if ( use_zero_pad )
			{
				for ( size_t i = normal_length; i < abs_field_width; i++ )
				{
					if ( callback(ctx, "0", 1) != 1 )
						return -1;
					written++;
				}
			}
			if ( use_precision )
			{
				for ( size_t i = digits_length; i < precision; i++ )
				{
					if ( callback(ctx, "0", 1) != 1 )
						return -1;
					written++;
				}
			}
			if ( callback(ctx, output, output_length) != output_length )
				return -1;
			written += output_length;
			if ( use_right_pad )
			{
				for ( size_t i = length_with_precision; i < abs_field_width; i++ )
				{
					if ( callback(ctx, " ", 1) != 1 )
						return -1;
					written++;
				}
			}
		}
#ifndef __is_sortix_libk
		else if ( *format == 'e' || *format == 'E' ||
		          *format == 'f' || *format == 'F' ||
		          *format == 'g' || *format == 'G' ||
		          *format == 'a' || *format == 'A' )
		{
			char conversion = *format++;

			long double value;
			if ( length == LENGTH_DEFAULT )
				value = va_arg(parameters, double);
			else if ( length == LENGTH_LONG_DOUBLE )
				value = va_arg(parameters, long double);
			else
				goto incomprehensible_conversion;

			// TODO: Implement floating-point printing.
			(void) conversion;
			(void) value;

			goto unsupported_conversion;
		}
#endif
		else if ( *format == 'c' && (format++, true) )
		{
			char c;
			if ( length == LENGTH_DEFAULT )
				c = (char) va_arg(parameters, int);
			else if ( length == LENGTH_LONG )
			{
				// TODO: Implement wide character printing.
				(void) va_arg(parameters, wint_t);
				goto unsupported_conversion;
			}
			else
				goto incomprehensible_conversion;

			if ( !field_width_is_negative && 1 < abs_field_width )
			{
				for ( size_t i = 1; i < abs_field_width; i++ )
				{
					if ( callback(ctx, " ", 1) != 1 )
						return -1;
					written++;
				}
			}

			if ( callback(ctx, &c, 1) != 1 )
				return -1;
			written++;

			if ( field_width_is_negative && 1 < abs_field_width )
			{
				for ( size_t i = 1; i < abs_field_width; i++ )
				{
					if ( callback(ctx, " ", 1) != 1 )
						return -1;
					written++;
				}
			}
		}
		else if ( *format == 'm' || *format == 's' )
		{
			char conversion = *format++;

			const char* string;
			if ( conversion == 'm' )
				string = strerror(errno), conversion = 's';
			else if ( length == LENGTH_DEFAULT )
				string = va_arg(parameters, const char*);
			else if ( length == LENGTH_LONG )
			{
				// TODO: Implement wide character string printing.
				(void) va_arg(parameters, const wchar_t*);
				goto unsupported_conversion;
			}
			else
				goto incomprehensible_conversion;

			if ( conversion == 's' && !string )
				string = "(null)";

			size_t string_length = 0;
			for ( size_t i = 0; i < precision && string[i]; i++ )
				string_length++;

			if ( !field_width_is_negative && string_length < abs_field_width )
			{
				for ( size_t i = string_length; i < abs_field_width; i++ )
				{
					if ( callback(ctx, " ", 1) != 1 )
						return -1;
					written++;
				}
			}

			if ( callback(ctx, string, string_length) != string_length )
				return -1;
			written += string_length;

			if ( field_width_is_negative && string_length < abs_field_width )
			{
				for ( size_t i = string_length; i < abs_field_width; i++ )
				{
					if ( callback(ctx, " ", 1) != 1 )
						return -1;
					written++;
				}
			}

		}
		else if ( *format == 'n' && (format++, true) )
		{
			if ( length == LENGTH_SHORT_SHORT )
				*va_arg(parameters, signed char*) = (signed char) written;
			else if ( length == LENGTH_SHORT )
				*va_arg(parameters, short*) = (short) written;
			else if ( length == LENGTH_DEFAULT )
				*va_arg(parameters, int*) = (int) written;
			else if ( length == LENGTH_LONG )
				*va_arg(parameters, long*) = (long) written;
			else if ( length == LENGTH_LONG_LONG )
				*va_arg(parameters, long long*) = (long long) written;
			else if ( length == LENGTH_INTMAX_T )
				*va_arg(parameters, uintmax_t*) = (uintmax_t) written;
			else if ( length == LENGTH_SIZE_T )
				*va_arg(parameters, size_t*) = (size_t) written;
			else if ( length == LENGTH_PTRDIFF_T )
				*va_arg(parameters, ptrdiff_t*) = (ptrdiff_t) written;
			else
				goto incomprehensible_conversion;
		}
		else
			goto incomprehensible_conversion;
	}

	if ( INT_MAX < written )
		return -1;

	return written;
}
