/*
 * Copyright (c) 2012 Nicolas George
 *
 * This file is part of FFmpeg.
 *
 * FFmpeg is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * FFmpeg is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with FFmpeg; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "sastring.h"
#include "bprint.h"
#include "va_copy.h"
#include "error.h"
#include "macros.h"
#include "mem.h"

#define sa_bprint_room(buf) ((buf)->size - SAMIN((buf)->len, (buf)->size))
#define sa_bprint_is_allocated(buf) ((buf)->str != (buf)->reserved_internal_buffer)

static int sa_bprint_alloc(AVBPrint *buf, unsigned room)
{
    char *old_str, *new_str;
    unsigned min_size, new_size;

    if (buf->size == buf->size_max)
        return AVERROR(EIO);
    if (!sa_bprint_is_complete(buf))
        return AVERROR_INVALIDDATA; /* it is already truncated anyway */
    min_size = buf->len + 1 + SAMIN(UINT_MAX - buf->len - 1, room);
    new_size = buf->size > buf->size_max / 2 ? buf->size_max : buf->size * 2;
    if (new_size < min_size)
        new_size = SAMIN(buf->size_max, min_size);
    old_str = sa_bprint_is_allocated(buf) ? buf->str : NULL;
    new_str = sa_realloc(old_str, new_size);
    if (!new_str)
        return AVERROR(ENOMEM);
    if (!old_str)
        memcpy(new_str, buf->str, buf->len + 1);
    buf->str  = new_str;
    buf->size = new_size;
    return 0;
}

static void sa_bprint_grow(AVBPrint *buf, unsigned extra_len)
{
    /* arbitrary margin to avoid small overflows */
    extra_len = SAMIN(extra_len, UINT_MAX - 5 - buf->len);
    buf->len += extra_len;
    if (buf->size)
        buf->str[SAMIN(buf->len, buf->size - 1)] = 0;
}

void sa_bprint_init(AVBPrint *buf, unsigned size_init, unsigned size_max)
{
    unsigned size_auto = (char *)buf + sizeof(*buf) -
                         buf->reserved_internal_buffer;

    if (size_max == SA_BPRINT_SIZE_AUTOMATIC)
        size_max = size_auto;
    buf->str      = buf->reserved_internal_buffer;
    buf->len      = 0;
    buf->size     = SAMIN(size_auto, size_max);
    buf->size_max = size_max;
    *buf->str = 0;
    if (size_init > buf->size)
        sa_bprint_alloc(buf, size_init - 1);
}

void sa_bprint_init_for_buffer(AVBPrint *buf, char *buffer, unsigned size)
{
    if (size == 0) {
        sa_bprint_init(buf, 0, SA_BPRINT_SIZE_COUNT_ONLY);
        return;
    }

    buf->str      = buffer;
    buf->len      = 0;
    buf->size     = size;
    buf->size_max = size;
    *buf->str = 0;
}

void sa_vbprintf(AVBPrint *buf, const char *fmt, va_list vl_arg)
{
    unsigned room;
    char *dst;
    int extra_len;
    va_list vl;

    while (1) {
        room = sa_bprint_room(buf);
        dst = room ? buf->str + buf->len : NULL;
        va_copy(vl, vl_arg);
        extra_len = vsnprintf(dst, room, fmt, vl);
        va_end(vl);
        if (extra_len <= 0)
            return;
        if (extra_len < room)
            break;
        if (sa_bprint_alloc(buf, extra_len))
            break;
    }
    sa_bprint_grow(buf, extra_len);
}

void sa_bprintf(AVBPrint *buf, const char *fmt, ...)
{
    va_list vl;
    va_start(vl, fmt);
    sa_vbprintf(buf, fmt, vl);
    va_end(vl);
}

void sa_bprint_chars(AVBPrint *buf, char c, unsigned n)
{
    unsigned room, real_n;

    while (1) {
        room = sa_bprint_room(buf);
        if (n < room)
            break;
        if (sa_bprint_alloc(buf, n))
            break;
    }
    if (room) {
        real_n = SAMIN(n, room - 1);
        memset(buf->str + buf->len, c, real_n);
    }
    sa_bprint_grow(buf, n);
}

void sa_bprint_append_data(AVBPrint *buf, const char *data, unsigned size)
{
    unsigned room, real_n;

    while (1) {
        room = sa_bprint_room(buf);
        if (size < room)
            break;
        if (sa_bprint_alloc(buf, size))
            break;
    }
    if (room) {
        real_n = SAMIN(size, room - 1);
        memcpy(buf->str + buf->len, data, real_n);
    }
    sa_bprint_grow(buf, size);
}

void sa_bprint_strftime(AVBPrint *buf, const char *fmt, const struct tm *tm)
{
    unsigned room;
    size_t l;
    size_t fmt_len = strlen(fmt);

    if (!*fmt)
        return;
    while (1) {
        room = sa_bprint_room(buf);
        if (room && (l = strftime(buf->str + buf->len, room, fmt, tm)))
            break;

        /* Due to the limitations of strftime() it is not possible to know if
         * the output buffer is too small or the output is empty.
         * However, a 256x output space requirement compared to the format
         * string length is so unlikely we can safely assume empty output. This
         * allows supporting possibly empty format strings like "%p". */
        if (room >> 8 > fmt_len)
            break;

        /* strftime does not tell us how much room it would need: let us
           retry with twice as much until the buffer is large enough */
        room = !room ? fmt_len + 1 :
               room <= INT_MAX / 2 ? room * 2 : INT_MAX;
        if (sa_bprint_alloc(buf, room)) {
            /* impossible to grow, try to manage something useful anyway */
            room = sa_bprint_room(buf);
            if (room < 1024) {
                /* if strftime fails because the buffer has (almost) reached
                   its maximum size, let us try in a local buffer; 1k should
                   be enough to format any real date+time string */
                char buf2[1024];
                if ((l = strftime(buf2, sizeof(buf2), fmt, tm))) {
                    sa_bprintf(buf, "%s", buf2);
                    return;
                }
            }
            if (room) {
                /* if anything else failed and the buffer is not already
                   truncated, let us add a stock string and force truncation */
                static const char txt[] = "[truncated strftime output]";
                memset(buf->str + buf->len, '!', room);
                memcpy(buf->str + buf->len, txt, SAMIN(sizeof(txt) - 1, room));
                sa_bprint_grow(buf, room); /* force truncation */
            }
            return;
        }
    }
    sa_bprint_grow(buf, l);
}

void sa_bprint_get_buffer(AVBPrint *buf, unsigned size,
                          unsigned char **mem, unsigned *actual_size)
{
    if (size > sa_bprint_room(buf))
        sa_bprint_alloc(buf, size);
    *actual_size = sa_bprint_room(buf);
    *mem = *actual_size ? buf->str + buf->len : NULL;
}

void sa_bprint_clear(AVBPrint *buf)
{
    if (buf->len) {
        *buf->str = 0;
        buf->len  = 0;
    }
}

int sa_bprint_finalize(AVBPrint *buf, char **ret_str)
{
    unsigned real_size = SAMIN(buf->len + 1, buf->size);
    char *str;
    int ret = 0;

    if (ret_str) {
        if (sa_bprint_is_allocated(buf)) {
            str = sa_realloc(buf->str, real_size);
            if (!str)
                str = buf->str;
            buf->str = NULL;
        } else {
            str = sa_memdup(buf->str, real_size);
            if (!str)
                ret = AVERROR(ENOMEM);
        }
        *ret_str = str;
    } else {
        if (sa_bprint_is_allocated(buf))
            sa_freep(&buf->str);
    }
    buf->size = real_size;
    return ret;
}

#define WHITESPACES " \n\t\r"

void sa_bprint_escape(AVBPrint *dstbuf, const char *src, const char *special_chars,
                      enum AVEscapeMode mode, int flags)
{
    const char *src0 = src;

    if (mode == SA_ESCAPE_MODE_AUTO)
        mode = SA_ESCAPE_MODE_BACKSLASH; /* TODO: implement a heuristic */

    switch (mode) {
    case SA_ESCAPE_MODE_QUOTE:
        /* enclose the string between '' */
        sa_bprint_chars(dstbuf, '\'', 1);
        for (; *src; src++) {
            if (*src == '\'')
                sa_bprintf(dstbuf, "'\\''");
            else
                sa_bprint_chars(dstbuf, *src, 1);
        }
        sa_bprint_chars(dstbuf, '\'', 1);
        break;

    case SA_ESCAPE_MODE_XML:
        /* escape XML non-markup character data as per 2.4 by default: */
        /*  [^<&]* - ([^<&]* ']]>' [^<&]*) */

        /* additionally, given one of the SA_ESCAPE_FLAG_XML_* flags, */
        /* escape those specific characters as required. */
        for (; *src; src++) {
            switch (*src) {
            case '&' : sa_bprintf(dstbuf, "%s", "&amp;");  break;
            case '<' : sa_bprintf(dstbuf, "%s", "&lt;");   break;
            case '>' : sa_bprintf(dstbuf, "%s", "&gt;");   break;
            case '\'':
                if (!(flags & SA_ESCAPE_FLAG_XML_SINGLE_QUOTES))
                    goto XML_DEFAULT_HANDLING;

                sa_bprintf(dstbuf, "%s", "&apos;");
                break;
            case '"' :
                if (!(flags & SA_ESCAPE_FLAG_XML_DOUBLE_QUOTES))
                    goto XML_DEFAULT_HANDLING;

                sa_bprintf(dstbuf, "%s", "&quot;");
                break;
XML_DEFAULT_HANDLING:
            default: sa_bprint_chars(dstbuf, *src, 1);
            }
        }
        break;

    /* case SA_ESCAPE_MODE_BACKSLASH or unknown mode */
    default:
        /* \-escape characters */
        for (; *src; src++) {
            int is_first_last       = src == src0 || !*(src+1);
            int is_ws               = !!strchr(WHITESPACES, *src);
            int is_strictly_special = special_chars && strchr(special_chars, *src);
            int is_special          =
                is_strictly_special || strchr("'\\", *src) ||
                (is_ws && (flags & SA_ESCAPE_FLAG_WHITESPACE));

            if (is_strictly_special ||
                (!(flags & SA_ESCAPE_FLAG_STRICT) &&
                 (is_special || (is_ws && is_first_last))))
                sa_bprint_chars(dstbuf, '\\', 1);
            sa_bprint_chars(dstbuf, *src, 1);
        }
        break;
    }
}
