/*
Copyright (C) 2010, Parrot Foundation.
$Id: stringbuilder.pmc 47435 2010-06-07 17:43:09Z NotFound $

=head1 NAME

src/pmc/stringbuilder.pmc - StringBuilder PMC Class

=head1 DESCRIPTION

TODO: Add description here.

=head2 Methods

=over 4

=cut

*/

#include "parrot/string_funcs.h"

/* HEADERIZER HFILE: none */
/* HEADERIZER BEGIN: static */
/* Don't modify between HEADERIZER BEGIN / HEADERIZER END.  Your changes will be lost. */

static size_t calculate_capacity(SHIM_INTERP,
    size_t current,
    size_t additional);

#define ASSERT_ARGS_calculate_capacity __attribute__unused__ int _ASSERT_ARGS_CHECK = (0)
/* Don't modify between HEADERIZER BEGIN / HEADERIZER END.  Your changes will be lost. */
/* HEADERIZER END: static */

#define INITIAL_STRING_CAPACITY 128

pmclass StringBuilder provides string auto_attrs {
    ATTR STRING *buffer;    /* Mutable string to gather results */

/*

=item C<void init()>

Initializes the StringBuilder.

=cut

*/

    VTABLE void init() {
        STATICSELF.init_int(INITIAL_STRING_CAPACITY);
    }

/*

=item C<void init_int()>

Initializes the StringBuilder with initial size of buffer.

=cut

*/

    VTABLE void init_int(INTVAL initial_size) {
        STRING * const buffer = mem_gc_allocate_zeroed_typed(INTERP, STRING);
        buffer->encoding  = Parrot_default_encoding_ptr;
        buffer->charset   = Parrot_default_charset_ptr;
        /* We need all string flags here because we use this buffer in substr_str */
        buffer->flags     = PObj_is_string_FLAG | PObj_live_FLAG | PObj_external_FLAG;
        buffer->_bufstart = buffer->strstart = mem_gc_allocate_n_typed(INTERP,
                initial_size, char);
        buffer->_buflen   = initial_size;

        SET_ATTR_buffer(INTERP, SELF, buffer);

        PObj_custom_destroy_SET(SELF);
    }

/*

=item C<void destroy()>

Free the buffer on destruction.

=cut

*/

    VTABLE void destroy() {
        if (PMC_data(SELF)) {
            STRING *buffer;
            GET_ATTR_buffer(INTERP, SELF, buffer);
            if (buffer->_bufstart)
                mem_gc_free(INTERP, buffer->_bufstart);
            mem_gc_free(INTERP, buffer);
        }
    }

/*

=item C<STRING *get_string()>

Returns created string.

=cut

*/

    VTABLE STRING *get_string() {
        STRING *buffer;
        GET_ATTR_buffer(INTERP, SELF, buffer);
        /* We need to clone buffer because outside of StringBuilder strings */
        /* are immutable */
        return Parrot_str_clone(INTERP, buffer);
    }

/*

=item C<STRING *push_string()>

Append string to current buffer.

=cut

*/

    VTABLE void push_string(STRING *s) {
        STRING          *buffer;
        size_t           total_size;
        const CHARSET   *cs;
        const ENCODING  *enc = NULL;

        /* Early return on NULL strings */
        if (STRING_IS_NULL(s))
            return;

        GET_ATTR_buffer(INTERP, SELF, buffer);

        /* If strings are incompatible - convert them */
        /* TODO Ask chromatic why in Parrot_str_join he ignored charset */
        cs = Parrot_str_rep_compatible(interp, buffer, s, &enc);
        if (!cs) {

            /* Create new temporary string */
            STRING * const new_buffer = Parrot_unicode_charset_ptr->to_charset(interp, buffer);
            mem_gc_free(INTERP, buffer->_bufstart);
            STRUCT_COPY(buffer, new_buffer);
            buffer->flags     = PObj_is_string_FLAG | PObj_live_FLAG | PObj_external_FLAG;

            buffer->_bufstart = buffer->strstart = mem_gc_allocate_n_typed(INTERP,
                                                    new_buffer->_buflen, char);
            mem_sys_memcopy(buffer->_bufstart, new_buffer->_bufstart, new_buffer->_buflen);

            SET_ATTR_buffer(INTERP, SELF, buffer);

            s      = Parrot_unicode_charset_ptr->to_charset(interp, s);
        }

        /* Calculate (possibly new) total size */
        total_size = calculate_capacity(INTERP, buffer->bufused, s->bufused);

        /* Reallocate if necessary */
        if (total_size > Buffer_buflen(buffer)) {
            /* Parrot_unicode_charset_ptr can produce NULL buffer */
            buffer->_bufstart = buffer->strstart = mem_gc_realloc_n_typed(INTERP,
                buffer->_bufstart, total_size, char);
            buffer->_buflen   = total_size;
        }

        /* Tack s on the end of buffer */
        mem_sys_memcopy((void *)((ptrcast_t)buffer->strstart + buffer->bufused),
                s->strstart, s->bufused);

        /* Update buffer */
        buffer->bufused += s->bufused;
        buffer->strlen  += Parrot_str_length(INTERP, s);
        buffer->hashval = 0; /* hash is invalid */

        PARROT_ASSERT(buffer->bufused <= Buffer_buflen(buffer));
    }

/*

=item C<VTABLE i_concatenate_str()>

=item C<VTABLE i_concatenate()>

Append string. Synonym for push_string

=cut

*/

    VTABLE void i_concatenate_str(STRING *s) {
        STATICSELF.push_string(s);
    }

    VTABLE void i_concatenate(PMC *p) {
        STATICSELF.push_string(VTABLE_get_string(INTERP, p));
    }

    VTABLE void push_pmc(PMC *p) {
        STATICSELF.push_string(VTABLE_get_string(INTERP, p));
    }

/*

=item C<VTABLE set_string_native(STRING)>

=item C<VTABLE set_pmc(PMC)>

Set content of buffer to passed string or PMC

=cut

*/
    VTABLE void set_string_native(STRING *s) {
        STRING * buffer;

        /* Calculate (possibly new) total size */
        size_t total_size = calculate_capacity(INTERP, 0, s->bufused);

        GET_ATTR_buffer(INTERP, SELF, buffer);

        /* Reallocate if necessary */
        if (total_size > Buffer_buflen(buffer)) {
            buffer->_bufstart = buffer->strstart = mem_gc_realloc_n_typed(INTERP,
                    buffer->_bufstart, total_size, char);
            buffer->_buflen   = total_size;
        }

        /* Tack s on the buffer */
        mem_sys_memcopy((void *)((ptrcast_t)buffer->strstart),
                s->strstart, s->bufused);

        /* Update buffer */
        buffer->bufused  = s->bufused;
        buffer->strlen   = Parrot_str_length(INTERP, s);
        buffer->encoding = s->encoding;
        buffer->charset  = s->charset;
    }

    VTABLE void set_pmc(PMC *s) {
        STATICSELF.set_string_native(VTABLE_get_string(INTERP, s));
    }


/*

=item C<VTABLE get_integer()>

Returns current capacity of allocated buffer.

For testing purpose only?

=cut

*/

    INTVAL get_integer() {
        STRING *buffer;
        GET_ATTR_buffer(INTERP, SELF, buffer);
        return Buffer_buflen(buffer);
    }

/*

=item C<VTABLE substr_str()>

=cut

*/

    VTABLE STRING *substr_str(INTVAL offset, INTVAL length) {
        STRING *buffer;
        GET_ATTR_buffer(INTERP, SELF, buffer);
        /* We must clone here becase we can reallocate buffer behind the scene... */
        /* TODO Optimize it to avoid creation of redundant STRING */
        return Parrot_str_clone(INTERP,
                Parrot_str_substr(INTERP, buffer, offset, length));
    }

/*

=item C<append_format(string fmt [, pmc args ] [, pmc hash ])>

Add a line to a C<StringBuilder> object according to C<fmt>.
The C<fmt> string can contain any number of "%-replacements"
which are replaced by the corresponding values from C<args>
or C<hash> prior to being appended to the string.  (Here
C<args> is a slurpy array, and C<hash> is a slurpy hash.)

The currently defined replacements include:

    %0 %1 ... %9     the value from the args array at index 0..9
    %,               the values of the args array separated by commas
    %%               a percent sign

A percent-sign followed by any other character that is a hash
key receives the value of the hash element.

=cut

*/

    METHOD append_format(STRING *fmt, PMC *args :slurpy, PMC *hash :slurpy :named) {
        STRING * const percent     = CONST_STRING(INTERP, "%");
        STRING * const comma       = CONST_STRING(INTERP, ",");
        STRING * const comma_space = CONST_STRING(INTERP, ", ");
        STRING * const newline     = CONST_STRING(INTERP, "\n");
        STRING *key;
        PMC    *stringbuilder = SELF;
        INTVAL percentPos;
        INTVAL pos = 0;

        /* Loop over the format string, splitting it into chunks
         * for the string builder. */
        while (pos >= 0) {
            /* Find the next % */
            percentPos = Parrot_str_find_index(INTERP, fmt, percent, pos);

            if (percentPos < 0) {
                if (pos == 0) {
                    VTABLE_push_string(INTERP, stringbuilder, fmt);
                }
                else {
                    /* remaining string can be added as is. */
                    VTABLE_push_string(INTERP, stringbuilder,
                        Parrot_str_substr(INTERP, fmt, pos,
                            Parrot_str_length(INTERP, fmt) -pos));
                    }
                break;
            }
            else {
                /* slurp up to just before the % sign... */
                VTABLE_push_string(INTERP, stringbuilder,
                    Parrot_str_substr(INTERP, fmt, pos, percentPos - pos));
                /* skip the % sign */
                pos = percentPos + 1 ;
            }

            /* key is always a single character */
            key = Parrot_str_substr(INTERP, fmt, pos++, 1);

            if (VTABLE_exists_keyed_str(INTERP, hash, key)) {
                VTABLE_push_string(INTERP, stringbuilder,
                        VTABLE_get_string_keyed_str(INTERP, hash, key));
            }
            else if (Parrot_str_is_cclass(INTERP, enum_cclass_numeric, key, 0)) {
                VTABLE_push_string(INTERP, stringbuilder,
                    VTABLE_get_string_keyed_int(INTERP, args,
                        Parrot_str_to_int(INTERP, key)));
            }
            else if (Parrot_str_equal(INTERP, key, comma)) {
                INTVAL num_args = VTABLE_elements(INTERP, args);
                INTVAL pos_args = 1;

                VTABLE_push_string(INTERP, stringbuilder,
                    VTABLE_get_string_keyed_int(INTERP, args, 0));

                while (pos_args < num_args) {
                    VTABLE_push_string(INTERP, stringbuilder, comma_space);
                    VTABLE_push_string(INTERP, stringbuilder,
                        VTABLE_get_string_keyed_int(INTERP, args, pos_args));
                    pos_args++;
                }
            }
            else if (Parrot_str_equal(INTERP, key, percent)) {
                VTABLE_push_string(INTERP, stringbuilder, percent);
            }
            else {
                /* %foo has no special meaning, pass it through unchanged */
                VTABLE_push_string(INTERP, stringbuilder,
                    Parrot_str_substr(INTERP, fmt, pos-2, 2));
            }
        }

        RETURN(PMC *SELF);
    }

/*
=item C<INTVAL get_string_length()>

Returns length of currently built string.

=cut
*/

    METHOD get_string_length() {
        STRING *buffer;
        INTVAL  length;
        GET_ATTR_buffer(INTERP, SELF, buffer);
        length = Parrot_str_length(INTERP, buffer);
        RETURN(INTVAL length);
    }


/*

=back

=cut

*/

}


/*

=head2 Helper functions.

=over 4

=cut

*/

/*

=item C<static size_t calculate_capacity(PARROT_INTERP, size_t current, size_t
additional)>

Calculate capacity for string. Usually StringBuilders used for "large"
strings. So capacity rounded up by next algorithm:
  - By 128 bytes if total capacity less then 1KB
  - By 1KB if total less than 4KB
  - By 4KB if total less than 1MB
  - By 1MB otherwise.

This function is subject for tuning on real-world usage scenarios.

=back

=cut

*/

static size_t
calculate_capacity(SHIM_INTERP, size_t current, size_t additional)
{
    ASSERT_ARGS(calculate_capacity)
    size_t total_size = current + additional;
    size_t chunk_size = 1024*1024;

    if (total_size < 1024)
        chunk_size = 128;
    else if (total_size < 4096)
        chunk_size = 1024;
    else if (total_size < 1024*1024)
        chunk_size = 4096;

    total_size = (total_size / chunk_size + 1) * chunk_size;

    return total_size;
}

/*
 * Local variables:
 *   c-file-style: "parrot"
 * End:
 * vim: expandtab shiftwidth=4:
 */
