/*
 * %CopyrightBegin%
 * 
 * Copyright Ericsson AB 2005-2009. All Rights Reserved.
 * 
 * The contents of this file are subject to the Erlang Public License,
 * Version 1.1, (the "License"); you may not use this file except in
 * compliance with the License. You should have received a copy of the
 * Erlang Public License along with this software. If not, it can be
 * retrieved online at http://www.erlang.org/.
 * 
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * %CopyrightEnd%
 */

/* Without this, variable argument lists break on VxWorks */
#ifdef VXWORKS
#include <vxWorks.h>
#endif

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <string.h>
#include "erl_errno.h"
#ifdef __WIN32__
#	include <io.h>
#else
#	include <unistd.h>
#endif
#include "erl_printf.h"
#include "erl_printf_format.h"

#ifdef DEBUG
#include <assert.h>
#define ASSERT(X) assert(X)
#else
#define ASSERT(X) 
#endif

#if defined(__WIN32__) && !defined(__GNUC__)
typedef int ssize_t;
#endif

int (*erts_printf_stdout_func)(char *, va_list) = NULL;
int (*erts_printf_stderr_func)(char *, va_list) = NULL;

int erts_printf_add_cr_to_stdout = 0;
int erts_printf_add_cr_to_stderr = 0;

int (*erts_printf_block_fpe)(void) = NULL;
void (*erts_printf_unblock_fpe)(int) = NULL;

#undef FLOCKFILE
#undef FUNLOCKFILE
#undef PUTC
#undef FWRITE
#undef PUTC_ON_SMALL_WRITES

#if defined(USE_THREADS) && defined(HAVE_FLOCKFILE)
#	define FLOCKFILE(FP)	flockfile(FP)
#	define FUNLOCKFILE(FP)	funlockfile(FP)
#	ifdef HAVE_PUTC_UNLOCKED
#		define PUTC	putc_unlocked
#		define PUTC_ON_SMALL_WRITES
#	endif
#	ifdef HAVE_FWRITE_UNLOCKED
#		define FWRITE	fwrite_unlocked
#	endif
#endif
#if !defined(USE_THREADS) && defined(putc) && !defined(fwrite)
#	define PUTC_ON_SMALL_WRITES
#endif
#if !defined(FLOCKFILE) || !defined(FUNLOCKFILE)
#	define FLOCKFILE(FP)
#	define FUNLOCKFILE(FP)
#endif
#ifndef PUTC
#	define PUTC putc
#endif
#ifndef FWRITE
#	define FWRITE fwrite
#endif

static int
get_error_result(void)
{
    int res = errno;
    if (res <= 0)
	res = EIO;
    return -res;
}


static int
write_f_add_cr(void *vfp, char* buf, size_t len)
{
    size_t i;
    ASSERT(vfp);
    for (i = 0; i < len; i++) {
	if (buf[i] == '\n' && PUTC('\r', (FILE *) vfp) == EOF)
	    return get_error_result();
	if (PUTC(buf[i], (FILE *) vfp) == EOF)
	    return get_error_result();
    }
    return 0;
}

static int
write_f(void *vfp, char* buf, size_t len)
{
    ASSERT(vfp);
#ifdef PUTC_ON_SMALL_WRITES
    if (len <= 64) { /* Try to optimize writes of small bufs. */
	int i;
	for (i = 0; i < len; i++)
	    if (PUTC(buf[i], (FILE *) vfp) == EOF)
		return get_error_result();
    }
    else
#endif
    if (FWRITE((void *) buf, sizeof(char), len, (FILE *) vfp) != len)
	return get_error_result();
    return 0;
}

static int
write_fd(void *vfdp, char* buf, size_t len)
{
    ssize_t size;
    ASSERT(vfdp);

    while (len) {
	size = write(*((int *) vfdp), (void *) buf, len);
	if (size < 0) {
#ifdef EINTR
	    if (errno == EINTR)
		continue;
#endif
	    return get_error_result();
	}
	if (size > len)
	    return -EIO;
	len -= size;
    }

    return 0;
}

static int
write_s(void *vwbufpp, char* bufp, size_t len)
{
    char **wbufpp = (char **) vwbufpp;
    ASSERT(wbufpp && *wbufpp);
    ASSERT(len > 0);
    memcpy((void *) *wbufpp, (void *) bufp, len);
    *wbufpp += len;
    return 0;
}


typedef struct {
    char *buf;
    size_t len;
} write_sn_arg_t;

static int
write_sn(void *vwsnap, char* buf, size_t len)
{
    write_sn_arg_t *wsnap = (write_sn_arg_t *) vwsnap;
    ASSERT(wsnap);
    ASSERT(len > 0);
    if (wsnap->len > 0) {
	size_t sz = len;
	if (sz >= wsnap->len)
	    sz = wsnap->len;
	memcpy((void *) wsnap->buf, (void *) buf, sz);
	wsnap->buf += sz;
	wsnap->len -= sz;
    }
    return 0;
}

static int
write_ds(void *vdsbufp, char* buf, size_t len)
{
    erts_dsprintf_buf_t *dsbufp = (erts_dsprintf_buf_t *) vdsbufp;
    size_t need_len = len + 1; /* Also trailing '\0' */
    ASSERT(dsbufp);
    ASSERT(len > 0);
    ASSERT(dsbufp->str_len <= dsbufp->size);
    if (need_len > dsbufp->size - dsbufp->str_len) {
	dsbufp = (*dsbufp->grow)(dsbufp, need_len);
	if (!dsbufp)
	    return -ENOMEM;
    }
    memcpy((void *) (dsbufp->str + dsbufp->str_len), (void *) buf, len);
    dsbufp->str_len += len;
    return 0;
}

int
erts_printf(const char *format, ...)
{
    int res;
    va_list arglist;
    va_start(arglist, format);
    errno = 0;
    if (erts_printf_stdout_func)
	res = (*erts_printf_stdout_func)((char *) format, arglist);
    else {
	FLOCKFILE(stdout);
	res = erts_printf_format(erts_printf_add_cr_to_stdout
				 ? write_f_add_cr
				 : write_f,
				 (void *) stdout,
				 (char *) format,
				 arglist);
	FUNLOCKFILE(stdout);
    }
    va_end(arglist);
    return res;
}

int
erts_fprintf(FILE *filep, const char *format, ...)
{
    int res;
    va_list arglist;
    va_start(arglist, format);
    errno = 0;
    if (erts_printf_stdout_func && filep == stdout)
	res = (*erts_printf_stdout_func)((char *) format, arglist);
    else if (erts_printf_stderr_func && filep == stderr)
	res = (*erts_printf_stderr_func)((char *) format, arglist);
    else {
	int (*fmt_f)(void*, char*, size_t);
	if (erts_printf_add_cr_to_stdout && filep == stdout)
	    fmt_f = write_f_add_cr;
	else if (erts_printf_add_cr_to_stderr && filep == stderr)
	    fmt_f = write_f_add_cr;
	else
	    fmt_f = write_f;
	FLOCKFILE(filep);
	res = erts_printf_format(fmt_f,(void *)filep,(char *)format,arglist);
	FUNLOCKFILE(filep);
    }
    va_end(arglist);
    return res;
}

int
erts_fdprintf(int fd, const char *format, ...)
{
    int res;
    va_list arglist;
    va_start(arglist, format);
    errno = 0;
    res = erts_printf_format(write_fd,(void *)&fd,(char *)format,arglist);
    va_end(arglist);
    return res;
}

int
erts_sprintf(char *buf, const char *format, ...)
{
    int res;
    char *p = buf;
    va_list arglist;
    va_start(arglist, format);
    errno = 0;
    res = erts_printf_format(write_s, (void *) &p, (char *) format, arglist);
    if (res < 0)
	buf[0] = '\0';
    else
	buf[res] = '\0';
    va_end(arglist);
    return res;
}

int
erts_snprintf(char *buf, size_t size, const char *format, ...)
{
    write_sn_arg_t wsnap;
    int res;
    va_list arglist;
    if (size < 1)
	return -EINVAL;
    wsnap.buf = buf;
    wsnap.len = size-1; /* Always need room for trailing '\0' */
    va_start(arglist, format);
    errno = 0;
    res = erts_printf_format(write_sn, (void *)&wsnap, (char *)format, arglist);
    if (res < 0)
	buf[0] = '\0';
    else if (res < size)
	buf[res] = '\0';
    else
	buf[size-1] = '\0';
    va_end(arglist);
    return res;
}
 
int
erts_dsprintf(erts_dsprintf_buf_t *dsbufp, const char *format, ...)
{
    int res;
    va_list arglist;
    if (!dsbufp)
	return -EINVAL;
    va_start(arglist, format);
    errno = 0;
    res = erts_printf_format(write_ds, (void *)dsbufp, (char *)format, arglist);
    if (dsbufp->str) {
	if (res < 0)
	    dsbufp->str[0] = '\0';
	else
	    dsbufp->str[dsbufp->str_len] = '\0';
    }
    va_end(arglist);
    return res;
}

int
erts_vprintf(const char *format, va_list arglist)
{	
    int res;
    if (erts_printf_stdout_func)
	res = (*erts_printf_stdout_func)((char *) format, arglist);
    else {
	errno = 0;
	res = erts_printf_format(erts_printf_add_cr_to_stdout
				 ? write_f_add_cr
				 : write_f,
				 (void *) stdout,
				 (char *) format,
				 arglist);
    }
    return res;
}

int
erts_vfprintf(FILE *filep, const char *format, va_list arglist)
{
    int res;
    if (erts_printf_stdout_func && filep == stdout)
	res = (*erts_printf_stdout_func)((char *) format, arglist);
    else if (erts_printf_stderr_func && filep == stderr)
	res = (*erts_printf_stderr_func)((char *) format, arglist);
    else {
	int (*fmt_f)(void*, char*, size_t);
	errno = 0;
	if (erts_printf_add_cr_to_stdout && filep == stdout)
	    fmt_f = write_f_add_cr;
	else if (erts_printf_add_cr_to_stderr && filep == stderr)
	    fmt_f = write_f_add_cr;
	else
	    fmt_f = write_f;
	res = erts_printf_format(fmt_f,(void *)filep,(char *)format,arglist);
    }
    return res;
}

int
erts_vfdprintf(int fd, const char *format, va_list arglist)
{
    int res;
    errno = 0;
    res = erts_printf_format(write_fd,(void *)&fd,(char *)format,arglist);
    return res;
}

int
erts_vsprintf(char *buf, const char *format, va_list arglist)
{
    int res;
    char *p = buf;
    errno = 0;
    res = erts_printf_format(write_s, (void *) &p, (char *) format, arglist);
    if (res < 0)
	buf[0] = '\0';
    else
	buf[res] = '\0';
    return res;
}

int
erts_vsnprintf(char *buf, size_t size, const char *format,  va_list arglist)
{
    write_sn_arg_t wsnap;
    int res;
    if (size < 1)
	return -EINVAL;
    wsnap.buf = buf;
    wsnap.len = size-1; /* Always need room for trailing '\0' */
    errno = 0;
    res = erts_printf_format(write_sn, (void *)&wsnap, (char *)format, arglist);
    if (res < 0)
	buf[0] = '\0';
    else if (res < size)
	buf[res] = '\0';
    else
	buf[size-1] = '\0';
    return res;
}

int
erts_vdsprintf(erts_dsprintf_buf_t *dsbufp, const char *format, va_list arglist)
{
    int res;
    if (!dsbufp)
	return -EINVAL;
    errno = 0;
    res = erts_printf_format(write_ds, (void *)dsbufp, (char *)format, arglist);
    if (dsbufp->str) {
	if (res < 0)
	    dsbufp->str[0] = '\0';
	else
	    dsbufp->str[dsbufp->str_len] = '\0';
    }
    return res;
}