/*
 * Copyright (c) 2008, 2009, 2010, 2011, 2012
 *   Jonathan Schleifer <js@webkeks.org>
 *
 * All rights reserved.
 *
 * This file is part of ObjFW. It may be distributed under the terms of the
 * Q Public License 1.0, which can be found in the file LICENSE.QPL included in
 * the packaging of this file.
 *
 * Alternatively, it may be distributed under the terms of the GNU General
 * Public License, either version 2 or 3, which can be found in the file
 * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this
 * file.
 */

#include "config.h"

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdbool.h>
#include <wchar.h>

#include <sys/types.h>

#import "OFString.h"

#import "asprintf.h"
#import "autorelease.h"
#import "macros.h"

#define MAX_SUBFORMAT_LEN 64

struct context {
	const char *format;
	size_t format_len;
	char subformat[MAX_SUBFORMAT_LEN + 1];
	size_t subformat_len;
	va_list arguments;
	char *buffer;
	size_t buffer_len;
	size_t i, last;
	enum {
		STATE_STRING,
		STATE_FORMAT_FLAGS,
		STATE_FORMAT_FIELD_WIDTH,
		STATE_FORMAT_LENGTH_MODIFIER,
		STATE_FORMAT_CONVERSION_SPECIFIER
	} state;
	enum {
		LENGTH_MODIFIER_NONE,
		LENGTH_MODIFIER_HH,
		LENGTH_MODIFIER_H,
		LENGTH_MODIFIER_L,
		LENGTH_MODIFIER_LL,
		LENGTH_MODIFIER_J,
		LENGTH_MODIFIER_Z,
		LENGTH_MODIFIER_T,
		LENGTH_MODIFIER_CAPITAL_L
	} length_modifier;
};

static bool
append_string(struct context *ctx, const char *append, size_t append_len)
{
	char *new_buf;

	if (append_len == 0)
		return true;

	if ((new_buf = realloc(ctx->buffer,
	    ctx->buffer_len + append_len + 1)) == NULL)
		return false;

	memcpy(new_buf + ctx->buffer_len, append, append_len);

	ctx->buffer = new_buf;
	ctx->buffer_len += append_len;

	return true;
}

static bool
append_subformat(struct context *ctx, const char *subformat,
    size_t subformat_len)
{
	if (ctx->subformat_len + subformat_len > MAX_SUBFORMAT_LEN)
		return false;

	memcpy(ctx->subformat + ctx->subformat_len, subformat, subformat_len);
	ctx->subformat_len += subformat_len;
	ctx->subformat[ctx->subformat_len] = 0;

	return true;
}

static bool
state_string(struct context *ctx)
{
	if (ctx->format[ctx->i] == '%') {
		if (ctx->i > 0)
			if (!append_string(ctx, ctx->format + ctx->last,
			    ctx->i - ctx->last))
				return false;

		if (!append_subformat(ctx, ctx->format + ctx->i, 1))
			return false;

		ctx->last = ctx->i + 1;
		ctx->state = STATE_FORMAT_FLAGS;
	}

	return true;
}

static bool
state_format_flags(struct context *ctx)
{
	switch (ctx->format[ctx->i]) {
	case '-':
	case '+':
	case ' ':
	case '#':
	case '0':
		if (!append_subformat(ctx, ctx->format + ctx->i, 1))
			return false;

		break;
	default:
		ctx->state = STATE_FORMAT_FIELD_WIDTH;
		ctx->i--;

		break;
	}

	return true;
}

static bool
state_format_field_width(struct context *ctx)
{
	if ((ctx->format[ctx->i] >= '0' && ctx->format[ctx->i] <= '9') ||
	    ctx->format[ctx->i] == '*' || ctx->format[ctx->i] == '.') {
		if (!append_subformat(ctx, ctx->format + ctx->i, 1))
			return false;
	} else {
		ctx->state = STATE_FORMAT_LENGTH_MODIFIER;
		ctx->i--;
	}

	return true;
}

static bool
state_format_length_modifier(struct context *ctx)
{
	/* Only one allowed */
	switch (ctx->format[ctx->i]) {
	case 'h': /* and also hh */
		if (ctx->format_len > ctx->i + 1 &&
		    ctx->format[ctx->i + 1] == 'h') {
			if (!append_subformat(ctx, ctx->format + ctx->i, 2))
				return false;

			ctx->i++;
			ctx->length_modifier = LENGTH_MODIFIER_HH;
		} else {
			if (!append_subformat(ctx, ctx->format + ctx->i, 1))
				return false;

			ctx->length_modifier = LENGTH_MODIFIER_H;
		}

		break;
	case 'l': /* and also ll */
		if (ctx->format_len > ctx->i + 1 &&
		    ctx->format[ctx->i + 1] == 'l') {
#ifndef _WIN32
			if (!append_subformat(ctx, ctx->format + ctx->i, 2))
				return false;
#else
			if (!append_subformat(ctx, "I64", 3))
				return false;
#endif

			ctx->i++;
			ctx->length_modifier = LENGTH_MODIFIER_LL;
		} else {
			if (!append_subformat(ctx, ctx->format + ctx->i, 1))
				return false;

			ctx->length_modifier = LENGTH_MODIFIER_L;
		}

		break;
	case 'j':
#ifndef _WIN32
		if (!append_subformat(ctx, ctx->format + ctx->i, 1))
			return false;
#else
		if (!append_subformat(ctx, "I64", 3))
			return false;
#endif

		ctx->length_modifier = LENGTH_MODIFIER_J;

		break;
	case 'z':
#ifndef _WIN32
		if (!append_subformat(ctx, ctx->format + ctx->i, 1))
			return false;
#else
		if (!append_subformat(ctx, "I", 1))
			return false;
#endif

		ctx->length_modifier = LENGTH_MODIFIER_Z;

		break;
	case 't':
#ifndef _WIN32
		if (!append_subformat(ctx, ctx->format + ctx->i, 1))
			return false;
#else
		if (!append_subformat(ctx, "I", 1))
			return false;
#endif

		ctx->length_modifier = LENGTH_MODIFIER_T;

		break;
	case 'L':
		if (!append_subformat(ctx, ctx->format + ctx->i, 1))
			return false;

		ctx->length_modifier = LENGTH_MODIFIER_CAPITAL_L;

		break;
#ifdef _WIN32
	case 'I': /* win32 strangeness (I64 instead of ll or j) */
		if (ctx->format_len > ctx->i + 2 &&
		    ctx->format[ctx->i + 1] == '6' &&
		    ctx->format[ctx->i + 2] == '4') {
			if (!append_subformat(ctx, ctx->format + ctx->i, 3))
				return false;

			ctx->i += 2;
			ctx->length_modifier = LENGTH_MODIFIER_LL;
		} else
			ctx->i--;

		break;
#endif
#ifdef OF_IOS
	case 'q': /* iOS uses this for PRI?64 */
		if (!append_subformat(ctx, ctx->format + ctx->i, 1))
			return false;

		ctx->length_modifier = LENGTH_MODIFIER_LL;

		break;
#endif
	default:
		ctx->i--;

		break;
	}

	ctx->state = STATE_FORMAT_CONVERSION_SPECIFIER;
	return true;
}

static bool
state_format_conversion_specifier(struct context *ctx)
{
	char *tmp = NULL;
	int tmp_len = 0;

	if (!append_subformat(ctx, ctx->format + ctx->i, 1))
		return false;

	switch (ctx->format[ctx->i]) {
	case '@':
		ctx->subformat[ctx->subformat_len - 1] = 's';

		@try {
			id object;

			if ((object = va_arg(ctx->arguments, id)) != nil) {
				void *pool = objc_autoreleasePoolPush();

				tmp_len = asprintf(&tmp, ctx->subformat,
				    [[object description] UTF8String]);

				objc_autoreleasePoolPop(pool);
			} else
				tmp_len = asprintf(&tmp, ctx->subformat,
				    "(nil)");
		} @catch (id e) {
			free(ctx->buffer);
			@throw e;
		}

		break;
	case 'd':
	case 'i':
		switch (ctx->length_modifier) {
		case LENGTH_MODIFIER_NONE:
		case LENGTH_MODIFIER_HH:
		case LENGTH_MODIFIER_H:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, int));
			break;
		case LENGTH_MODIFIER_L:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, long));
			break;
		case LENGTH_MODIFIER_LL:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, long long));
			break;
		case LENGTH_MODIFIER_J:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, intmax_t));
			break;
		case LENGTH_MODIFIER_Z:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, ssize_t));
			break;
		case LENGTH_MODIFIER_T:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, ptrdiff_t));
			break;
		default:
			return false;
		}

		break;
	case 'o':
	case 'u':
	case 'x':
	case 'X':
		switch (ctx->length_modifier) {
		case LENGTH_MODIFIER_NONE:
		case LENGTH_MODIFIER_HH:
		case LENGTH_MODIFIER_H:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, unsigned int));
			break;
		case LENGTH_MODIFIER_L:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, unsigned long));
			break;
		case LENGTH_MODIFIER_LL:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, unsigned long long));
			break;
		case LENGTH_MODIFIER_J:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, uintmax_t));
			break;
		case LENGTH_MODIFIER_Z:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, size_t));
			break;
		case LENGTH_MODIFIER_T:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, ptrdiff_t));
			break;
		default:
			return false;
		}

		break;
	case 'f':
	case 'F':
	case 'e':
	case 'E':
	case 'g':
	case 'G':
	case 'a':
	case 'A':
		switch (ctx->length_modifier) {
		case LENGTH_MODIFIER_NONE:
		case LENGTH_MODIFIER_L:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, double));
			break;
		case LENGTH_MODIFIER_CAPITAL_L:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, long double));
			break;
		default:
			return false;
		}

		break;
	case 'c':
		switch (ctx->length_modifier) {
		case LENGTH_MODIFIER_NONE:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, int));
			break;
		case LENGTH_MODIFIER_L:
#if WINT_MAX >= INT_MAX
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, wint_t));
#else
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, int));
#endif
			break;
		default:
			return false;
		}

		break;
	case 's':
		switch (ctx->length_modifier) {
		case LENGTH_MODIFIER_NONE:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, const char*));
			break;
		case LENGTH_MODIFIER_L:
			tmp_len = asprintf(&tmp, ctx->subformat,
			    va_arg(ctx->arguments, const wchar_t*));
			break;
		default:
			return false;
		}

		break;
	case 'p':
		if (ctx->length_modifier != LENGTH_MODIFIER_NONE)
			return false;

		tmp_len = asprintf(&tmp, ctx->subformat,
		    va_arg(ctx->arguments, void*));

		break;
	case 'n':
		switch (ctx->length_modifier) {
		case LENGTH_MODIFIER_NONE:
			*va_arg(ctx->arguments, int*) =
			    (int)ctx->buffer_len;
			break;
		case LENGTH_MODIFIER_HH:
			*va_arg(ctx->arguments, signed char*) =
			    (signed char)ctx->buffer_len;
			break;
		case LENGTH_MODIFIER_H:
			*va_arg(ctx->arguments, short*) =
			    (short)ctx->buffer_len;
			break;
		case LENGTH_MODIFIER_L:
			*va_arg(ctx->arguments, long*) =
			    (long)ctx->buffer_len;
			break;
		case LENGTH_MODIFIER_LL:
			*va_arg(ctx->arguments, long long*) =
			    (long long)ctx->buffer_len;
			break;
		case LENGTH_MODIFIER_J:
			*va_arg(ctx->arguments, intmax_t*) =
			    (intmax_t)ctx->buffer_len;
			break;
		case LENGTH_MODIFIER_Z:
			*va_arg(ctx->arguments, size_t*) =
			    (size_t)ctx->buffer_len;
			break;
		case LENGTH_MODIFIER_T:
			*va_arg(ctx->arguments, ptrdiff_t*) =
			    (ptrdiff_t)ctx->buffer_len;
			break;
		default:
			return false;
		}

		break;
	case '%':
		if (ctx->length_modifier != LENGTH_MODIFIER_NONE)
			return false;

		if (!append_string(ctx, "%", 1))
			return false;

		break;
	default:
		return false;
	}

	if (tmp_len == -1)
		return false;

	if (tmp != NULL) {
		if (!append_string(ctx, tmp, tmp_len)) {
			free(tmp);
			return false;
		}

		free(tmp);
	}

	memset(ctx->subformat, 0, MAX_SUBFORMAT_LEN);
	ctx->subformat_len = 0;
	ctx->length_modifier = LENGTH_MODIFIER_NONE;

	ctx->last = ctx->i + 1;
	ctx->state = STATE_STRING;

	return true;
}

static bool (*states[])(struct context*) = {
	state_string,
	state_format_flags,
	state_format_field_width,
	state_format_length_modifier,
	state_format_conversion_specifier
};

int
of_vasprintf(char **string, const char *format, va_list arguments)
{
	struct context ctx;

	ctx.format = format;
	ctx.format_len = strlen(format);
	memset(ctx.subformat, 0, MAX_SUBFORMAT_LEN + 1);
	ctx.subformat_len = 0;
	va_copy(ctx.arguments, arguments);
	ctx.buffer_len = 0;
	ctx.last = 0;
	ctx.state = STATE_STRING;
	ctx.length_modifier = LENGTH_MODIFIER_NONE;

	if ((ctx.buffer = malloc(1)) == NULL)
		return -1;

	for (ctx.i = 0; ctx.i < ctx.format_len; ctx.i++) {
		if (!states[ctx.state](&ctx)) {
			free(ctx.buffer);
			return -1;
		}
	}

	if (ctx.state != STATE_STRING) {
		free(ctx.buffer);
		return -1;
	}

	if (!append_string(&ctx, ctx.format + ctx.last,
	    ctx.format_len - ctx.last)) {
		free(ctx.buffer);
		return -1;
	}

	ctx.buffer[ctx.buffer_len] = 0;

	*string = ctx.buffer;
	return (ctx.buffer_len <= INT_MAX ? (int)ctx.buffer_len : -1);
}

int
of_asprintf(char **string, const char *format, ...)
{
	va_list arguments;
	int ret;

	va_start(arguments, format);
	ret = of_vasprintf(string, format, arguments);
	va_end(arguments);

	return ret;
}
