/*
 * %CopyrightBegin%
 * 
 * Copyright Ericsson AB 2001-2023. All Rights Reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * %CopyrightEnd%
 *

 */
/*
 * Function:
 * ei_format to build binary format terms a bit like printf
 */

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

#ifdef VRTX
#define __READY_EXTENSIONS__
#include <errno.h>
#endif

#include "eidef.h"
#include "ei_malloc.h"
#include "ei_format.h"

/*
 * To avoid problems we read the variable number of arguments to an
 * array of unions.
 */
union arg {
  char c;
  char* s;
  long l;
  unsigned long u;
  double d;
  erlang_pid* pid;
};

static int eiformat(const char** s, union arg** args, ei_x_buff* x);

/* forwards of parse functions */
static int pformat(const char** fmt, union arg**, ei_x_buff* x);
static int plist(const char** fmt, union arg**, ei_x_buff* x, int size);
static int ptuple(const char** fmt, union arg**, ei_x_buff* x, int size);
static int pquotedatom(const char** fmt, ei_x_buff* x);
static int pdigit(const char** fmt, ei_x_buff* x);
static int patom(const char** fmt, ei_x_buff* x);
static int pstring(const char** fmt, ei_x_buff* x);
static int pmap(const char ** fmt, union arg **, ei_x_buff * x);

/* format a string into an ei_x_buff, except the version token */
static int eiformat(const char** fmt, union arg** args, ei_x_buff* x)
{
    const char* p = *fmt;
    int res;
    ei_x_buff x2;

    while (isspace((int)*p))
	++p;
    switch (*p) {
    case '~':
	res = pformat(&p, args, x);
	break;
    case '[':
	res = ei_x_new(&x2);
	if (res >= 0)
	    res = plist(&p, args, &x2, 0);
	if (res > 0)
	    res = ei_x_encode_list_header(x, res);
	if (res >= 0)
	    res = ei_x_append(x, &x2);
	ei_x_free(&x2);
	break;
    case '{':
	res = ei_x_new(&x2);
	if (res >= 0)
	    res = ptuple(&p, args, &x2, 0);
	if (res >= 0)
	    res = ei_x_encode_tuple_header(x, res);
	if (res >= 0)
	    res = ei_x_append(x, &x2);
	ei_x_free(&x2);
	break;
    case '"':
	res = pstring(&p, x);
	break;
    case '\'':
	res = pquotedatom(&p, x);
	break;
    case '#':
	if (*(p + 1) == '{') {
            p += 2;
	    res = ei_x_new(&x2);
	    if (res >= 0)
                res = pmap(&p, args, &x2);
	    if (res >= 0)
		res = ei_x_encode_map_header(x, res);
	    if (res >= 0)
		res = ei_x_append(x, &x2);
	    ei_x_free(&x2);
	    break;
	}
    default:
	if (isdigit((int)*p))
	    res = pdigit(&p, x);
	else if ((*p == '-' || *p == '+') && isdigit((int)*(p+1)))
	    res = pdigit(&p, x);
	else if (islower((int)*p))
	    res = patom(&p, x);
	else
	    res = -1;
	break;
	/*
	Variables
	*/
    }
    *fmt = p;
    return res;
}

static int patom(const char** fmt, ei_x_buff* x)
{
    const char* start = *fmt;
    char c;
    int len;
    
    for (;;) {
	c = *(*fmt)++;
	if (isalnum((int) c) || (c == '_') || (c == '@'))
	    continue;
	else
	    break;
    }
    --(*fmt);
    len = *fmt - start;
    /* FIXME why truncate atom name and not fail?! */
    if (len >= MAXATOMLEN)
	len = MAXATOMLEN-1;
    return ei_x_encode_atom_len(x, start, len);
}

/* Check if integer or float */
static int pdigit(const char** fmt, ei_x_buff* x)
{
    const char* start = *fmt;
    char c;
    int dotp=0;
    double d;
    long l;

    if (**fmt == '-' || **fmt == '+')
	(*fmt)++;
    for (;;) {
	c = *(*fmt)++;
	if (isdigit((int)c))
	    continue;
	else if (!dotp && (c == '.')) {
	    dotp = 1;
	    continue;
	} else
	    break;
    } 
    --(*fmt);
    if (dotp) {
	sscanf(start, "%lf", &d);
	return ei_x_encode_double(x, d);
    } else {
	sscanf(start, "%ld", &l);
	return ei_x_encode_long(x, l);
    }
}

/* "string" */
static int pstring(const char** fmt, ei_x_buff* x)
{
    const char* start = ++(*fmt); /* skip first quote */
    char c;
    int res;
    
    for (;;) {
	c = *(*fmt)++;
	if (c == '\0')
	    return -1;
	if (c == '"') {
	    if (*((*fmt)-1) == '\\')
		continue;
	    else
		break;
	} else
	    continue;
    }
    res = ei_x_encode_string_len(x, start, *fmt - start - 1);
    return res;
}

/* 'atom' */
static int pquotedatom(const char** fmt, ei_x_buff* x)
{
    const char* start = ++(*fmt); /* skip first quote */
    char c;
    int res;
    
    for (;;) {
	c = *(*fmt)++;
	if (c == 0)
	    return -1;
	if (c == '\'') {
	    if (*((*fmt)-1) == '\\')
		continue;
	    else
		break;
	} else 
	    continue;
    } 
    res = ei_x_encode_atom_len(x, start, *fmt - start - 1);
    return res;
}


 /* 
  * The format letters are:
  *   a  -  An atom
  *   c  -  A character
  *   s  -  A string
  *   i  -  An integer
  *   l  -  A long integer
  *   u  -  An unsigned long integer
  *   f  -  A float 
  *   d  -  A double float 
  *   p  -  An Erlang PID
  */
static int pformat(const char** fmt, union arg** args, ei_x_buff* x)
{
    int res = 0;

    ASSERT(args && *args);

    ++(*fmt);	/* skip tilde */
    switch (*(*fmt)++) {
    case 'a': 
	res = ei_x_encode_atom(x, (*args)->s);
	(*args)++;
	break;
    case 'c':
	res = ei_x_encode_char(x, (*args)->c);
	(*args)++;
	break;
    case 's':
	res = ei_x_encode_string(x, (*args)->s);
	(*args)++;
	break;
    case 'i':
	res = ei_x_encode_long(x, (*args)->l);
	(*args)++;
	break;
    case 'l':
	res = ei_x_encode_long(x, (*args)->l);
	(*args)++;
	break;
    case 'u':
	res = ei_x_encode_ulong(x, (*args)->u);
	(*args)++;
	break;
    case 'f':     /* float is expanded to double (C calling conventions) */
    case 'd':
	res = ei_x_encode_double(x, (*args)->d);
	(*args)++;
	break;	
    case 'p':
	res = ei_x_encode_pid(x, (*args)->pid);
	(*args)++;
	break;
    default:
	res = -1;
	break;
    }
    return res;
}

/* encode a tuple */
static int ptuple(const char** fmt, union arg** args, ei_x_buff* x, int size)
{
    int res = 0;
    const char* p = *fmt;
    char after = *p++;
    
    if (after == '}') {
	*fmt = p;
	return size;
    }
    while (isspace((int)*p))
	++p;
    switch (*p++) {
    case '}':
	if (after == ',')
	    res = -1;
	else
	    res = size;
	break;
    case ',':
	if (after == ',' || after == '{')
	    res = -1;
	else
	    res = ptuple(&p, args, x, size);
	break;
    default:
	--p;
	res = eiformat(&p, args, x);
	if (res >= 0)
	    res = ptuple(&p, args, x, size + 1);
	break;
	/*
	Variables
	*/
    }
    *fmt = p;
    return res;
}

/* encode a list */
static int plist(const char** fmt, union arg** args, ei_x_buff* x, int size)
{
    int res = 0;
    const char* p = *fmt;
    char after = *p++;

    if (after == ']')
	--p;
    while (isspace((int)*p))
	++p;
    switch (*p++) {
    case ']':
	if (after == ',')
	    res = -1;
	else {
	    if (after != '|')
		ei_x_encode_empty_list(x);
	    res = size;
	}
	break;
    case '|':
	if (after == '|' || after == ',')
	    res = -1;
	else
	    res = plist(&p, args, x, size);
	break;
    case ',':
	if (after == '|' || after == ',')
	    res = -1;
	else
	    res = plist(&p, args, x, size);
	break;
    default:
	--p;
	res = eiformat(&p, args, x);
	++size;
	if (res >= 0) {
	    if (after == '|') {
	        while (isspace((int)*p))
		    ++p;
		if (*p != ']')
		    res = -1;
	    } else
		res = plist(&p, args, x, size);
	}
	break;
	/*
	Variables
	*/
    }
    *fmt = p;
    return res;
}

/* encode a map */
static int pmap(const char ** fmt, union arg ** args, ei_x_buff * x)
{
    const char * p = *fmt;
    int size = 0;

    while (isspace(*p))
        ++p;

    if (*p == '}') {
	*fmt = p+1;
	return size;
    }

    for(;;) {
        /* Key */
        if (eiformat(&p, args, x) < 0)
            return -1;
        while (isspace(*p))
            ++p;

        if (!(p[0] == '=' && p[1] == '>')) {
            return -1;
        }
        p += 2;

        /* Value */
        if (eiformat(&p, args, x) < 0)
            return -1;
        while (isspace(*p))
            ++p;

        ++size;
        if (*p == '}') {
            *fmt = p+1;
            return size;
        }
        if (*p++ != ',')
            return -1;
        while (isspace((int)*p))
            ++p;
    }
}

static int read_args(const char* fmt, va_list ap, union arg **argp)
{
    const char* p = fmt;
    int arg_count = 0;
    union arg* args;
    int i = 0;

    /* Count the number of format strings. Assume null terminated string. */

    *argp = NULL;

    while (*p) if (*p++ == '~') arg_count++;


    if (!arg_count) {
	return 0;
    }
    /* Allocate space for the arguments */

    args = (union arg*)ei_malloc(arg_count * sizeof(union arg));

    if (!args) 
	return -1;

    p = fmt;			/* Start again and fill array */

    while (*p) {
      if (*p++ == '~') {
	if (!*p) {
	  ei_free(args);
	  return -1;	/* Error, string not complete */
	}
	switch (*p++) {
	case 'c':
	  args[i++].c = (char) va_arg(ap, int);
	  break;
	case 'a': 
	case 's':
	  args[i++].s = va_arg(ap, char*);
	  break;
	case 'i':
#ifdef EI_64BIT	  
	  args[i++].l = (long) va_arg(ap, int);
	  break;
#endif
	case 'l':
	  args[i++].l = va_arg(ap, long);
	  break;
	case 'u':
	  args[i++].u = va_arg(ap, unsigned long);
	  break;
	case 'f':     /* float is expanded to double (C calling conventions) */
	case 'd':
	  args[i++].d = va_arg(ap, double);
	  break;	
	case 'p':
	  args[i++].pid = va_arg(ap, erlang_pid*);
	  break;
	default:
	  ei_free(args);	/* Invalid specifier */
	  return -1;
	}
      }
    }
    *argp = args;
    return 0;
}
       
int ei_x_format(ei_x_buff* x, const char* fmt, ... )
{
    va_list ap;
    union arg* args;
    union arg* saved_args; 
    int res;

    res = ei_x_encode_version(x);
    if (res < 0) return res;

    va_start(ap, fmt);
    res = read_args(fmt,ap,&args);
    saved_args = args;
    va_end(ap);
    if (res < 0) {
	return -1;
    }

    res = eiformat(&fmt, &args, x);
    ei_free(saved_args);

    return res;
}

int ei_x_format_wo_ver(ei_x_buff* x, const char* fmt, ... )
{
    va_list ap;
    union arg* args; 
    union arg* saved_args; 
    int res;

    va_start(ap, fmt);
    res = read_args(fmt,ap,&args);
    saved_args = args;
    va_end(ap);
    if (res < 0) {
	return -1;
    }
    res = eiformat(&fmt, &args, x);
    ei_free(saved_args);

    return res;
}
