/**
 * \file
 *
 * \brief Console core layer
 *
 * This is the console core layer, providing simple buffering support
 * and a printf() implementation.
 *
 * - Compiler:           IAR EWAVR32 and GNU GCC for AVR32
 * - Supported devices:  All devices
 * - AppNote:
 *
 * \author               Atmel Corporation: http://www.atmel.com \n
 *                       Support and FAQ: http://support.atmel.no/
 *
 * \page License
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. The name of Atmel may not be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * 4. This software may only be redistributed and used in connection with an
 * Atmel AVR product.
 *
 * THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE
 * EXPRESSLY AND SPECIFICALLY DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */
#include <assert.h>
#include <console/core.h>
#include <console/driver.h>
#include <interrupt.h>
#include <stdarg.h>
#include <string.h>
#include <util.h>

/**
 * \brief vprintf() format conversion state.
 *
 * This indicates what the parser will be looking for next. The parser
 * may pass through several states for each character in the format
 * conversion spec, as many of the fields are optional.
 */
enum conversion_state {
	/** Normal state, passing characters through to the console. */
	STATE_NORMAL,
	/** Parsing an optional conversion flag. */
	STATE_FLAG,
	/** Parsing an optional field width specifier. */
	STATE_WIDTH,
	/** Looking for a period indicating a precision specifier. */
	STATE_PERIOD,
	/** Parsing an optional precision specifier. */
	STATE_PRECISION,
	/** Parsing an optional length modifier. */
	STATE_LENGTH,
	/** Parsing the conversion specifier. */
	STATE_CONVSPEC,
};

/**
 * \brief Conversion data for vprintf().
 */
struct printf_conversion {
	/** Minimum field width, or 0 if unspecified. */
	int width;
	/** Minimum precision, or 0 if unspecified. */
	int precision;
	/** Length modifier. This can be 'h', 'l' or 0 (default.) */
	char length;
	/** Conversion specifier. */
	char spec;
	/** Character to use for padding to specified width */
	char pad_char;
	/** Conversion argument extracted from \a ap. */
	union {
		/** Signed integer argument. */
		long d;
		/** Unsigned integer argument. */
		unsigned long u;
		/** Floating-point argument. */
		double f;
		/** String argument. */
		const char *s;
		/** Pointer argument. */
		void *p;
		/**
		 * Argument indicating where to store the result of a
		 * %n conversion.
		 */
		int *n;
	} arg;
};

/**
 * \brief Push one character onto the output buffer.
 *
 * \param drv The console driver which will eventually handle the data.
 * \param c Character to be appended to the buffer.
 */
static void console_drv_putchar(struct console_driver *drv, char c)
{
	unsigned long	iflags;

	if (c == '\n')
		console_drv_putchar(drv, '\r');

	iflags = cpu_irq_save();
	while (console_buf_unused(&drv->tx_buf) < 1) {
		cpu_irq_restore(iflags);
		drv->tx_make_room(drv, 1);
		iflags = cpu_irq_save();
	}

	console_buf_insert_char(&drv->tx_buf, c);
	cpu_irq_restore(iflags);
}

/**
 * \brief Write one or more characters to the output buffer.
 *
 * \param drv The console driver which will eventually handle the data.
 * \param data Pointer to the character buffer.
 * \param len Length of the character buffer in bytes.
 */
static void console_drv_write(struct console_driver *drv,
		const char *data, size_t len)
{
	size_t partial;
	unsigned long head;
	unsigned long iflags;

	while (len) {
		if (console_buf_unused(&drv->tx_buf) < len)
			drv->tx_make_room(drv, len);

		iflags = cpu_irq_save();
		head = ring_get_head(&drv->tx_buf.ring, CONSOLE_BUF_SIZE);
		partial = min(len, console_buf_unused_before_end(&drv->tx_buf));
		memcpy(drv->tx_buf.data + head, data, partial);
		ring_insert_entries(&drv->tx_buf.ring, partial);
		cpu_irq_restore(iflags);

		data += partial;
		len -= partial;
	}
}

/**
 * \brief Push a nul-terminated string onto the output buffer.
 *
 * \param drv The console driver which will eventually handle the data.
 * \param str Nul-terminated string to be appended to the buffer.
 */
static int console_drv_putstr(struct console_driver *drv, const char *str)
{
	int len;

	len = strlen(str);
	console_drv_write(drv, str, len);

	return len;
}

/**
 * \brief Commit the output buffer to the driver.
 *
 * This will tell the console driver that the output buffer contains
 * new data, which may trigger the driver to push the data to the
 * hardware.
 *
 * \param drv The console driver which will eventually handle the data.
 */
static void console_drv_commit(struct console_driver *drv)
{
	drv->tx_commit(drv);
}

/**
 * \brief Write a string to a console.
 *
 * \param con The console instance.
 * \param str NUL-terminated string.
 *
 * \return The number of characters written.
 */
int console_putstr(struct console *con, const char *str)
{
	int len = console_drv_putstr(con->drv, str);
	console_drv_commit(con->drv);
	return len;
}

/**
 * \brief Write a single character to a console.
 *
 * \param con The console instance.
 * \param c The character to write.
 *
 * \return \a c as an unsigned char cast to an int.
 */
int console_putchar(struct console *con, int c)
{
	struct console_driver *drv = con->drv;
	char ch = c;

	console_drv_putchar(drv, ch);
	console_drv_commit(drv);

	return ch;
}

/**
 * \brief Print a signed integer according to the conversion
 * specification.
 *
 * \param drv The console driver that handles the output.
 * \param conv conversion data parsed from the format string.
 *
 * \return The number of characters printed.
 */
static int console_drv_print_signed(struct console_driver *drv,
		struct printf_conversion *conv)
{
	char buf[32];
	long number = conv->arg.d;
	int negative = 0;
	int i = sizeof(buf);
	int len;
	char c;

	if (number == 0) {
		console_drv_putchar(drv, '0');
		return 1;
	}

	if (number < 0) {
		negative = 1;
		number = -number;
	}

	while (number) {
		c = '0' + number % 10;
		number /= 10;
		buf[--i] = c;
	}

	if (negative)
		buf[--i] = '-';

	if (conv->width > sizeof(buf))
		conv->width = sizeof(buf);

	while ((sizeof(buf) - i) < conv->width)
		buf[--i] = conv->pad_char;

	len = sizeof(buf) - i;
	console_drv_write(drv, buf + i, len);

	return len;
}

/**
 * \brief Print an unsigned integer according to the conversion
 * specification.
 *
 * \param drv The console driver that handles the output.
 * \param conv Conversion data parsed from the format string.
 *
 * \return The number of characters printed.
 */
static int console_drv_print_unsigned(struct console_driver *drv,
		struct printf_conversion *conv)
{
	char buf[32];
	unsigned long number = conv->arg.u;
	int i = sizeof(buf);
	int len;
	char c;

	if (number == 0)
		buf[--i] = '0';

	switch (conv->spec) {
	case 'o':
		while (number) {
			c = '0' + (number & 7);
			number >>= 3;
			buf[--i] = c;
		}
		break;
	case 'u':
		while (number) {
			c = '0' + (number % 10);
			number /= 10;
			buf[--i] = c;
		}
		break;
	case 'x':
		while (number) {
			if ((number & 15) > 9)
				c = 'a' - 10 + (number & 15);
			else
				c = '0' + (number & 15);
			number >>= 4;
			buf[--i] = c;
		}
		break;
	case 'X':
		while (number) {
			if ((number & 15) > 9)
				c = 'A' - 10 + (number & 15);
			else
				c = '0' + (number & 15);
			number >>= 4;
			buf[--i] = c;
		}
		break;
	}

	if (conv->width > sizeof(buf))
		conv->width = sizeof(buf);

	while ((sizeof(buf) - i) < conv->width)
		buf[--i] = conv->pad_char;

	len = sizeof(buf) - i;
	console_drv_write(drv, buf + i, len);

	return len;
}

/**
 * \brief Formatted output conversion.
 *
 * Produce output according to \a format on the given console. \a
 * format is interpreted as a regular printf()-style format string
 * with a few limitations (some specifiers are accepted but ignored.)
 *
 * \param con The console instance.
 * \param format Format specification.
 * \param ap Format arguments as a vararg list.
 *
 * \return The number of characters printed.
 */
int console_vprintf(struct console *con, const char *format, va_list ap)
{
	int state = STATE_NORMAL;
	struct printf_conversion conv;
	struct console_driver *drv = con->drv;
	int n = 0;
	char c;

	while ((c = *format++)) {
		switch (state) {
		case STATE_NORMAL:
			if (c == '%') {
				state = STATE_FLAG;
				conv.width = 0;
				conv.precision = 0;
				conv.length = 0;
				conv.pad_char = ' ';
			} else {
				console_drv_putchar(drv, c);
				n++;
			}
			break;

		case STATE_FLAG:
			state = STATE_WIDTH;

			/* We accept all standard flags, but we ignore some */
			switch (c) {
			case '0':
				conv.pad_char = '0';
				break;
			case '#':
			case '-':
			case ' ':
			case '+':
				break;

			case '%':
				/* %% -> output a literal '%' */
				console_drv_putchar(drv, c);
				n++;
				state = STATE_NORMAL;
				break;

			default:
				goto state_width;
			}
			break;

		state_width:
		case STATE_WIDTH:
			if (isdigit(c) && (c != '0' || conv.width != 0)) {
				conv.width *= 10;
				conv.width += c - '0';
				break;
			}

			state = STATE_PERIOD;
			/* fall through */

		case STATE_PERIOD:
			if (c != '.') {
				state = STATE_LENGTH;
				goto state_length;
			}
			state = STATE_PRECISION;
			break;

		case STATE_PRECISION:
			/* accept but ignore */
			if (isdigit(c))
				break;

			state = STATE_LENGTH;
			/* fall through */

		state_length:
		case STATE_LENGTH:
			/* SUSv2 only accepts h, l and L */
			if (c == 'h' || c == 'l' || c == 'L') {
				conv.length = c;
				break;
			} else if (c == 'z') {
				if (sizeof(size_t) == sizeof(long))
					conv.length = 'l';
				break;
			}

			state = STATE_CONVSPEC;
			/* fall through */

		case STATE_CONVSPEC:
			conv.spec = c;

			switch (c) {
			case 'd':
			case 'i':
				if (conv.length == 'l')
					conv.arg.d = va_arg(ap, long);
				else
					conv.arg.d = va_arg(ap, int);
				n += console_drv_print_signed(drv, &conv);
				break;
			case 'o':
			case 'u':
			case 'x':
			case 'X':
				if (conv.length == 'l')
					conv.arg.u = va_arg(ap, unsigned long);
				else
					conv.arg.u = va_arg(ap, unsigned int);
				n += console_drv_print_unsigned(drv, &conv);
				break;
			case 'c':
				conv.arg.d = va_arg(ap, int);
				console_drv_putchar(drv, conv.arg.d);
				n++;
				break;

			/* TODO: Handle floats if needed */

			case 's':
				conv.arg.s = va_arg(ap, const char *);
				n += console_drv_putstr(drv, conv.arg.s);
				break;
			case 'p':
				conv.arg.p = va_arg(ap, void *);
				console_drv_write(drv, "0x", 2);
				n += 2;
				conv.spec = 'x';
				n += console_drv_print_unsigned(drv, &conv);
				break;
			case 'n':
				conv.arg.n = va_arg(ap, int *);
				*conv.arg.n = n;
				break;
			}

			state = STATE_NORMAL;
			break;
		}
	}

	console_drv_commit(drv);

	return n;
}

/**
 * \brief Formatted output conversion.
 *
 * This is simply a convenience wrapper around console_vprintf()
 * taking a variable number of arguments.
 */
int console_printf(struct console *con, const char *format, ...)
{
	int n;
	va_list ap;

	va_start(ap, format);
	n = console_vprintf(con, format, ap);
	va_end(ap);

	return n;
}
