The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#include "EXTERN.h"
#include "perl.h"
#define NO_XSLOCKS
#include "XSUB.h"
#include "ppport.h"

#include <stdint.h>
#include <libxml/parser.h>
#ifdef XMLHASH_HAVE_ICONV
#include <iconv.h>
#endif
#ifdef XMLHASH_HAVE_ICU
#include <unicode/utypes.h>
#include <unicode/ucnv.h>
#endif

#ifndef MUTABLE_PTR
#if defined(__GNUC__) && !defined(PERL_GCC_BRACE_GROUPS_FORBIDDEN)
#  define MUTABLE_PTR(p) ({ void *_p = (p); _p; })
#else
#  define MUTABLE_PTR(p) ((void *) (p))
#endif
#endif

#ifndef MUTABLE_SV
#define MUTABLE_SV(p)   ((SV *)MUTABLE_PTR(p))
#endif

#if __GNUC__ >= 3
# define expect(expr,value)         __builtin_expect ((expr), (value))
# define INLINE                     static inline
#else
# define expect(expr,value)         (expr)
# define INLINE                     static
#endif

#define expect_false(expr) expect ((expr) != 0, 0)
#define expect_true(expr)  expect ((expr) != 0, 1)

#define FLAG_SIMPLE                     1
#define FLAG_COMPLEX                    2
#define FLAG_CONTENT                    4
#define FLAG_ATTR_ONLY                  8

#define MAX_RECURSION_DEPTH             128

#define BUFFER_WRITE(str, len)          XMLHash_writer_write(writer, str, len)
#define BUFFER_WRITE_CONSTANT(str)      XMLHash_writer_write(writer, str, sizeof(str) - 1)
#define BUFFER_WRITE_STRING(str,len)    XMLHash_writer_write(writer, str, len)
#define BUFFER_WRITE_ESCAPE(str, len)   XMLHash_writer_escape_content(writer, str, len)
#define BUFFER_WRITE_ESCAPE_ATTR(str)   XMLHash_writer_escape_attr(writer, str)
#define BUFFER_WRITE_QUOTED(str)        XMLHash_writer_write_quoted_string(writer, str)

#ifndef FALSE
#define FALSE (0)
#endif

#ifndef TRUE
#define TRUE  (1)
#endif

#define CONV_DEF_OUTPUT    NULL
#define CONV_DEF_METHOD    "NATIVE"
#define CONV_DEF_ROOT      "root"
#define CONV_DEF_VERSION   "1.0"
#define CONV_DEF_ENCODING  "utf-8"
#define CONV_DEF_INDENT    0
#define CONV_DEF_CANONICAL FALSE
#define CONV_DEF_USE_ATTR  FALSE
#define CONV_DEF_CONTENT   ""
#define CONV_DEF_XML_DECL  TRUE
#define CONV_DEF_DOC       FALSE

#define CONV_DEF_ATTR      "-"
#define CONV_DEF_TEXT      "#text"
#define CONV_DEF_TRIM      TRUE
#define CONV_DEF_CDATA     ""
#define CONV_DEF_COMM      ""

#define CONV_STR_PARAM_LEN 32

#define CONV_READ_PARAM_INIT                            \
    SV   *sv;                                           \
    char *str;
#define CONV_READ_STRING_PARAM(var, name, def_value)    \
    if ( (sv = get_sv(name, 0)) != NULL ) {             \
        if ( SvOK(sv) ) {                               \
            str = (char *) SvPV_nolen(sv);              \
            strncpy(var, str, CONV_STR_PARAM_LEN);      \
        }                                               \
        else {                                          \
            var[0] = '\0';                              \
        }                                               \
    }                                                   \
    else {                                              \
        strncpy(var, def_value, CONV_STR_PARAM_LEN);    \
    }
#define CONV_READ_BOOL_PARAM(var, name, def_value)      \
    if ( (sv = get_sv(name, 0)) != NULL ) {             \
        if ( SvTRUE(sv) ) {                             \
            var = TRUE;                                 \
        }                                               \
        else {                                          \
            var = FALSE;                                \
        }                                               \
    }                                                   \
    else {                                              \
        var = def_value;                                \
    }
#define CONV_READ_INT_PARAM(var, name, def_value)       \
    if ( (sv = get_sv(name, 0)) != NULL ) {             \
        var = SvIV(sv);                                 \
    }                                                   \
    else {                                              \
        var = def_value;                                \
    }
#define CONV_READ_REF_PARAM(var, name, def_value)       \
    if ( (sv = get_sv(name, 0)) != NULL ) {             \
        if ( SvOK(sv) && SvROK(sv) ) {                  \
            var = sv;                                   \
        }                                               \
        else {                                          \
            var = NULL;                                 \
        }                                               \
    }                                                   \
    else {                                              \
        var = def_value;                                \
    }

#define str3cmp(p, c0, c1, c2)                                                \
    *(uint32_t *) p == ((c2 << 16) | (c1 << 8) | c0)

#define str4cmp(p, c0, c1, c2, c3)                                            \
    *(uint32_t *) p == ((c3 << 24) | (c2 << 16) | (c1 << 8) | c0)

#define str5cmp(p, c0, c1, c2, c3, c4)                                        \
    *(uint32_t *) p == ((c3 << 24) | (c2 << 16) | (c1 << 8) | c0)             \
        && p[4] == c4

#define str6cmp(p, c0, c1, c2, c3, c4, c5)                                    \
    *(uint32_t *) p == ((c3 << 24) | (c2 << 16) | (c1 << 8) | c0)             \
        && (((uint32_t *) p)[1] & 0xffff) == ((c5 << 8) | c4)

#define str7cmp(p, c0, c1, c2, c3, c4, c5, c6)                                \
    *(uint32_t *) p == ((c3 << 24) | (c2 << 16) | (c1 << 8) | c0)             \
        && ((uint32_t *) p)[1] == ((c6 << 16) | (c5 << 8) | c4)

#define str8cmp(p, c0, c1, c2, c3, c4, c5, c6, c7)                            \
    *(uint32_t *) p == ((c3 << 24) | (c2 << 16) | (c1 << 8) | c0)             \
        && ((uint32_t *) p)[1] == ((c7 << 24) | (c6 << 16) | (c5 << 8) | c4)

#define str9cmp(p, c0, c1, c2, c3, c4, c5, c6, c7, c8)                        \
    *(uint32_t *) p == ((c3 << 24) | (c2 << 16) | (c1 << 8) | c0)             \
        && ((uint32_t *) p)[1] == ((c7 << 24) | (c6 << 16) | (c5 << 8) | c4)  \
        && p[8] == c8

typedef uintptr_t bool_t;

typedef enum {
    CONV_METHOD_NATIVE = 0,
    CONV_METHOD_NATIVE_ATTR_MODE,
    CONV_METHOD_LX
} convMethodType;

typedef struct _conv_buffer_t conv_buffer_t;
struct _conv_buffer_t {
    SV                    *scalar;
    char                  *start;
    char                  *cur;
    char                  *end;
};

#if defined(XMLHASH_HAVE_ICONV) || defined(XMLHASH_HAVE_ICU)
typedef enum {
    ENC_ICONV,
    ENC_ICU
} encoderType;

typedef struct _conv_encoder_t conv_encoder_t;
struct _conv_encoder_t {
    encoderType type;
#ifdef XMLHASH_HAVE_ICONV
    iconv_t     iconv;
#endif
#ifdef XMLHASH_HAVE_ICU
    UConverter *uconv; /* for conversion between an encoding and UTF-16 */
    UConverter *utf8;  /* for conversion between UTF-8 and UTF-16 */
#endif
};
#endif

typedef struct _conv_writer_t conv_writer_t;
struct _conv_writer_t {
#if defined(XMLHASH_HAVE_ICONV) || defined(XMLHASH_HAVE_ICU)
    conv_encoder_t        *encoder;
    conv_buffer_t          enc_buf;
#endif
    PerlIO                *perl_io;
    SV                    *perl_obj;
    conv_buffer_t          main_buf;
};

struct _conv_opts_t {
    convMethodType         method;

    /* native options */
    char                   version[CONV_STR_PARAM_LEN];
    char                   encoding[CONV_STR_PARAM_LEN];
    char                   root[CONV_STR_PARAM_LEN];
    bool_t                 xml_decl;
    bool_t                 canonical;
    char                   content[CONV_STR_PARAM_LEN];
    int                    indent;
    void                  *output;
    bool_t                 doc;

    /* LX options */
    char                   attr[CONV_STR_PARAM_LEN];
    int                    attr_len;
    char                   text[CONV_STR_PARAM_LEN];
    bool_t                 trim;
    char                   cdata[CONV_STR_PARAM_LEN];
    char                   comm[CONV_STR_PARAM_LEN];
};
typedef struct _conv_opts_t conv_opts_t;

typedef enum {
    TAG_OPEN,
    TAG_CLOSE,
    TAG_EMPTY,
    TAG_START,
    TAG_END
} tagType;

typedef struct {
    char *key;
    void *value;
} hash_entity_t;

typedef struct _stash_entity_t stash_entity_t;
struct _stash_entity_t {
    void                   *data;
    struct _stash_entity_t *next;
};

typedef struct {
    conv_opts_t        opts;
    int                recursion_depth;
    int                indent_count;
    stash_entity_t     stash;
    conv_writer_t     *writer;
} convert_ctx_t;

const char indent_string[60] = "                                                            ";

INLINE void XMLHash_write_item_no_attr(convert_ctx_t *ctx, char *name, SV *value);
INLINE void XMLHash_write_item_no_attr2doc(convert_ctx_t *ctx, char *name, SV *value, xmlNodePtr rootNode);
INLINE int  XMLHash_write_item(convert_ctx_t *ctx, char *name, SV *value, int flag);
INLINE void XMLHash_write_hash(convert_ctx_t *ctx, char *name, SV *hash);
INLINE void XMLHash_write_hash2doc(convert_ctx_t *ctx, char *name, SV *hash, xmlNodePtr rootNode);
INLINE void XMLHash_write_hash_lx(convert_ctx_t *ctx, SV *hash, int flag);
INLINE void XMLHash_write_hash_lx2doc(convert_ctx_t *ctx, SV *hash, int flag, xmlNodePtr rootNode);

#define Pmm_NO_PSVI      0
#define Pmm_PSVI_TAINTED 1

struct _ProxyNode {
    xmlNodePtr node;
    xmlNodePtr owner;
    int count;
};

struct _DocProxyNode {
    xmlNodePtr node;
    xmlNodePtr owner;
    int count;
    int encoding; /* only used for proxies of xmlDocPtr */
    int psvi_status; /* see below ... */
};

/* helper type for the proxy structure */
typedef struct _DocProxyNode DocProxyNode;
typedef struct _ProxyNode ProxyNode;

/* pointer to the proxy structure */
typedef ProxyNode* ProxyNodePtr;
typedef DocProxyNode* DocProxyNodePtr;

/* this my go only into the header used by the xs */
#define SvPROXYNODE(x) (INT2PTR(ProxyNodePtr,SvIV(SvRV(x))))
#define PmmPROXYNODE(x) (INT2PTR(ProxyNodePtr,x->_private))
#define SvNAMESPACE(x) (INT2PTR(xmlNsPtr,SvIV(SvRV(x))))

#define x_PmmREFCNT(node)      node->count
#define x_PmmREFCNT_inc(node)  node->count++
#define x_PmmNODE(xnode)       xnode->node
#define x_PmmOWNER(node)       node->owner
#define x_PmmOWNERPO(node)     ((node && x_PmmOWNER(node)) ? (ProxyNodePtr)x_PmmOWNER(node)->_private : node)

#define x_PmmENCODING(node)    ((DocProxyNodePtr)(node))->encoding
#define x_PmmNodeEncoding(node) ((DocProxyNodePtr)(node->_private))->encoding

#define x_SetPmmENCODING(node,code) x_PmmENCODING(node)=(code)
#define x_SetPmmNodeEncoding(node,code) x_PmmNodeEncoding(node)=(code)

#define x_PmmSvNode(n) x_PmmSvNodeExt(n,1)

#define x_PmmUSEREGISTRY       (x_PROXY_NODE_REGISTRY_MUTEX != NULL)
#define x_PmmREGISTRY          (INT2PTR(xmlHashTablePtr,SvIV(SvRV(get_sv("XML::LibXML::__PROXY_NODE_REGISTRY",0)))))

ProxyNodePtr
x_PmmNewNode(xmlNodePtr node);

SV*
x_PmmNodeToSv( xmlNodePtr node, ProxyNodePtr owner );

SV* x_PROXY_NODE_REGISTRY_MUTEX = NULL;

const char*
x_PmmNodeTypeName( xmlNodePtr elem ){
    const char *name = "XML::LibXML::Node";

    if ( elem != NULL ) {
        switch ( elem->type ) {
        case XML_ELEMENT_NODE:
            name = "XML::LibXML::Element";
            break;
        case XML_TEXT_NODE:
            name = "XML::LibXML::Text";
            break;
        case XML_COMMENT_NODE:
            name = "XML::LibXML::Comment";
            break;
        case XML_CDATA_SECTION_NODE:
            name = "XML::LibXML::CDATASection";
            break;
        case XML_ATTRIBUTE_NODE:
            name = "XML::LibXML::Attr";
            break;
        case XML_DOCUMENT_NODE:
        case XML_HTML_DOCUMENT_NODE:
            name = "XML::LibXML::Document";
            break;
        case XML_DOCUMENT_FRAG_NODE:
            name = "XML::LibXML::DocumentFragment";
            break;
        case XML_NAMESPACE_DECL:
            name = "XML::LibXML::Namespace";
            break;
        case XML_DTD_NODE:
            name = "XML::LibXML::Dtd";
            break;
        case XML_PI_NODE:
            name = "XML::LibXML::PI";
            break;
        default:
            name = "XML::LibXML::Node";
            break;
        };
        return name;
    }
    return "";
}

ProxyNodePtr
x_PmmNewNode(xmlNodePtr node)
{
    ProxyNodePtr proxy = NULL;

    if ( node == NULL ) {
        return NULL;
    }

    if ( node->_private == NULL ) {
        switch ( node->type ) {
        case XML_DOCUMENT_NODE:
        case XML_HTML_DOCUMENT_NODE:
        case XML_DOCB_DOCUMENT_NODE:
            proxy = (ProxyNodePtr)xmlMalloc(sizeof(struct _DocProxyNode));
            if (proxy != NULL) {
                ((DocProxyNodePtr)proxy)->psvi_status = Pmm_NO_PSVI;
                x_SetPmmENCODING(proxy, XML_CHAR_ENCODING_NONE);
            }
            break;
        default:
            proxy = (ProxyNodePtr)xmlMalloc(sizeof(struct _ProxyNode));
            break;
        }
        if (proxy != NULL) {
            proxy->node  = node;
            proxy->owner   = NULL;
            proxy->count   = 0;
            node->_private = (void*) proxy;
        }
    }
    else {
        proxy = (ProxyNodePtr)node->_private;
    }

    return proxy;
}

SV*
x_PmmNodeToSv( xmlNodePtr node, ProxyNodePtr owner )
{
    ProxyNodePtr dfProxy= NULL;
    SV * retval = &PL_sv_undef;
    const char * CLASS = "XML::LibXML::Node";

    if ( node != NULL ) {
#ifdef XML_LIBXML_THREADS
        if( x_PmmUSEREGISTRY )
            SvLOCK(x_PROXY_NODE_REGISTRY_MUTEX);
#endif
        /* find out about the class */
        CLASS = x_PmmNodeTypeName( node );

        if ( node->_private != NULL ) {
            dfProxy = x_PmmNewNode(node);
            /* warn(" at 0x%08.8X\n", dfProxy); */
        }
        else {
            dfProxy = x_PmmNewNode(node);
            /* fprintf(stderr, " at 0x%08.8X\n", dfProxy); */
            if ( dfProxy != NULL ) {
                if ( owner != NULL ) {
                    dfProxy->owner = x_PmmNODE( owner );
                    x_PmmREFCNT_inc( owner );
                    /* fprintf(stderr, "REFCNT incremented on owner: 0x%08.8X\n", owner); */
                }
            }
            else {
                warn("x_PmmNodeToSv:   proxy creation failed!\n");
            }
        }

        retval = NEWSV(0,0);
        sv_setref_pv( retval, CLASS, (void*)dfProxy );
#ifdef XML_LIBXML_THREADS
    if( x_PmmUSEREGISTRY )
        x_PmmRegistryREFCNT_inc(dfProxy);
#endif
        x_PmmREFCNT_inc(dfProxy);
        /* fprintf(stderr, "REFCNT incremented on node: 0x%08.8X\n", dfProxy); */

        switch ( node->type ) {
        case XML_DOCUMENT_NODE:
        case XML_HTML_DOCUMENT_NODE:
        case XML_DOCB_DOCUMENT_NODE:
            if ( ((xmlDocPtr)node)->encoding != NULL ) {
                x_SetPmmENCODING(dfProxy, (int)xmlParseCharEncoding( (const char*)((xmlDocPtr)node)->encoding ));
            }
            break;
        default:
            break;
        }
#ifdef XML_LIBXML_THREADS
        if( x_PmmUSEREGISTRY )
            SvUNLOCK(x_PROXY_NODE_REGISTRY_MUTEX);
#endif
    }
    else {
        warn( "x_PmmNodeToSv: no node found!\n" );
    }

    return retval;
}

#ifdef XMLHASH_HAVE_ICU
void
XMLHash_encoder_uconv_destroy(UConverter *uconv)
{
    if (uconv != NULL) {
        ucnv_close(uconv);
    }
}

UConverter *
XMLHash_encoder_uconv_create(char *encoding, int toUnicode)
{
    UConverter *uconv;
    UErrorCode  status = U_ZERO_ERROR;

    uconv = ucnv_open(encoding, &status);
    if ( U_FAILURE(status) ) {
        return NULL;
    }

    if (toUnicode) {
        ucnv_setToUCallBack(uconv, UCNV_TO_U_CALLBACK_STOP,
                            NULL, NULL, NULL, &status);
    }
    else {
        ucnv_setFromUCallBack(uconv, UCNV_FROM_U_CALLBACK_STOP,
                              NULL, NULL, NULL, &status);
    }

    return uconv;
}
#endif

#if defined(XMLHASH_HAVE_ICONV) || defined(XMLHASH_HAVE_ICU)
void
XMLHash_encoder_destroy(conv_encoder_t *encoder)
{
    if (encoder != NULL) {
#ifdef XMLHASH_HAVE_ICONV
        if (encoder->iconv != NULL) {
            iconv_close(encoder->iconv);
        }
#endif

#ifdef XMLHASH_HAVE_ICU
        XMLHash_encoder_uconv_destroy(encoder->uconv);
        XMLHash_encoder_uconv_destroy(encoder->utf8);
#endif
        free(encoder);
    }
}

conv_encoder_t *
XMLHash_encoder_create(char *encoding)
{
    conv_encoder_t *encoder;

    encoder = malloc(sizeof(conv_encoder_t));
    if (encoder == NULL) {
        return NULL;
    }
    memset(encoder, 0, sizeof(conv_encoder_t));

#ifdef XMLHASH_HAVE_ICONV
    encoder->iconv = iconv_open(encoding, "UTF-8");
    if (encoder->iconv != (iconv_t) -1) {
        encoder->type = ENC_ICONV;
        return encoder;
    }
    iconv_close(encoder->iconv);
    encoder->iconv = NULL;
#endif

#ifdef XMLHASH_HAVE_ICU
    encoder->uconv = XMLHash_encoder_uconv_create(encoding, 1);
    if (encoder->uconv != NULL) {
        encoder->utf8 = XMLHash_encoder_uconv_create("UTF-8", 0);
        if (encoder->utf8 != NULL) {
            encoder->type = ENC_ICU;
            return encoder;
        }
    }
#endif

    XMLHash_encoder_destroy(encoder);

    return NULL;
}
#endif

void
XMLHash_writer_buffer_init(conv_buffer_t *buf, int size)
{
    buf->scalar = newSV(size);
    sv_setpv(buf->scalar, "");

    buf->start = buf->cur = SvPVX(buf->scalar);
    buf->end   = buf->start + size;
}

void
XMLHash_writer_buffer_resize(conv_buffer_t *buf, int inc)
{
    if (inc <= (buf->end - buf->cur)) {
        return;
    }

    int size = buf->end - buf->start;
    int use  = buf->cur - buf->start;

    size += inc < size ? size : inc;

    SvCUR_set(buf->scalar, use);
    SvGROW(buf->scalar, size);

    buf->start = SvPVX(buf->scalar);
    buf->cur   = buf->start + use;
    buf->end   = buf->start + size;
}

INLINE void
XMLHash_writer_write_to_perl_obj(conv_buffer_t *buf, SV *perl_obj)
{
    int len = buf->cur - buf->start;

    if (len > 0) {
        *buf->cur = '\0';
        SvCUR_set(buf->scalar, len);

        dSP;

        ENTER;
        SAVETMPS;

        PUSHMARK(SP);
        EXTEND(SP, 2);
        PUSHs((SV *) perl_obj);
        PUSHs(buf->scalar);
        PUTBACK;

        call_method("PRINT", G_SCALAR);

        FREETMPS;
        LEAVE;

        buf->cur = buf->start;
    }
}

INLINE void
XMLHash_writer_write_to_perl_io(conv_buffer_t *buf, PerlIO *perl_io)
{
    int len = buf->cur - buf->start;

    if (len > 0) {
        *buf->cur = '\0';
        SvCUR_set(buf->scalar, len);

        PerlIO_write(perl_io, buf->start, len);

        buf->cur = buf->start;
    }
}

INLINE SV *
XMLHash_writer_write_to_perl_scalar(conv_buffer_t *buf)
{
    *buf->cur = '\0';
    SvCUR_set(buf->scalar, buf->cur - buf->start);

    return buf->scalar;
}

SV *
XMLHash_writer_flush_buffer(conv_writer_t *writer, conv_buffer_t *buf)
{
    if (writer->perl_obj != NULL) {
        XMLHash_writer_write_to_perl_obj(buf, writer->perl_obj);
        return &PL_sv_undef;
    }
    else if (writer->perl_io != NULL) {
        XMLHash_writer_write_to_perl_io(buf, writer->perl_io);
        return &PL_sv_undef;
    }

    return XMLHash_writer_write_to_perl_scalar(buf);
}

#if defined(XMLHASH_HAVE_ICONV) || defined(XMLHASH_HAVE_ICU)
void
XMLHash_writer_encode_buffer(conv_writer_t *writer, conv_buffer_t *main_buf, conv_buffer_t *enc_buf)
{
    int   len  = (main_buf->cur - main_buf->start) * 4 + 1;
    char *src  = main_buf->start;

    if (len > (enc_buf->end - enc_buf->cur)) {
        XMLHash_writer_flush_buffer(writer, enc_buf);

        XMLHash_writer_buffer_resize(enc_buf, len);
    }

#ifdef XMLHASH_HAVE_ICONV
    if (writer->encoder->type == ENC_ICONV) {
        size_t in_left  = main_buf->cur - main_buf->start;
        size_t out_left = enc_buf->end - enc_buf->cur;

        size_t converted = iconv(writer->encoder->iconv, &src, &in_left, &enc_buf->cur, &out_left);
        if (converted == (size_t) -1) {
            croak("Convert error");
        }
        return;
    }
#endif

#ifdef XMLHASH_HAVE_ICU
    UErrorCode  err  = U_ZERO_ERROR;
    ucnv_convertEx(writer->encoder->uconv, writer->encoder->utf8, &enc_buf->cur, enc_buf->end,
                   (const char **) &src, main_buf->cur, NULL, NULL, NULL, NULL,
                   FALSE, TRUE, &err);

    if ( U_FAILURE(err) ) {
        croak("Convert error: %d", err);
    }
#endif
}
#endif

SV *
XMLHash_writer_flush(conv_writer_t *writer)
{
    conv_buffer_t *buf;

#if defined(XMLHASH_HAVE_ICONV) || defined(XMLHASH_HAVE_ICU)
    if (writer->encoder != NULL) {
        XMLHash_writer_encode_buffer(writer, &writer->main_buf, &writer->enc_buf);
        buf = &writer->enc_buf;
    }
    else {
        buf = &writer->main_buf;
    }
#else
    buf = &writer->main_buf;
#endif

    return XMLHash_writer_flush_buffer(writer, buf);
}

void
XMLHash_writer_resize_buffer(conv_writer_t *writer, int inc)
{
    (void) XMLHash_writer_flush(writer);

    XMLHash_writer_buffer_resize(&writer->main_buf, inc);
}

INLINE void
XMLHash_writer_destroy(conv_writer_t *writer)
{
    if (writer != NULL) {
        if (writer->perl_obj != NULL || writer->perl_io != NULL) {
            SvREFCNT_dec(writer->main_buf.scalar);
#if defined(XMLHASH_HAVE_ICONV) || defined(XMLHASH_HAVE_ICU)
            SvREFCNT_dec(writer->enc_buf.scalar);
        }
        else if (writer->encoder != NULL) {
            SvREFCNT_dec(writer->main_buf.scalar);
#endif
        }

#if defined(XMLHASH_HAVE_ICONV) || defined(XMLHASH_HAVE_ICU)
        XMLHash_encoder_destroy(writer->encoder);
#endif
        free(writer);
    }
}

INLINE conv_writer_t *
XMLHash_writer_create(conv_opts_t *opts, int size)
{
    conv_writer_t *writer;

    writer = malloc(sizeof(conv_writer_t));
    if (writer == NULL) {
        croak("Memory allocation error");
    }
    memset(writer, 0, sizeof(conv_writer_t));

    XMLHash_writer_buffer_init(&writer->main_buf, size);

    if (strcasecmp(opts->encoding, "UTF-8") != 0) {
#if defined(XMLHASH_HAVE_ICONV) || defined(XMLHASH_HAVE_ICU)
        writer->encoder = XMLHash_encoder_create(opts->encoding);
        if (writer->encoder == NULL) {
            croak("Can't create encoder for '%s'", opts->encoding);
        }

        XMLHash_writer_buffer_init(&writer->enc_buf, size * 4);
#else
        croak("Can't create encoder for '%s'", opts->encoding);
#endif
    }

    if (opts->output != NULL) {
        MAGIC  *mg;
        GV     *gv = (GV *) opts->output;
        IO     *io = GvIO(gv);

        if (io && (mg = SvTIED_mg((SV *)io, PERL_MAGIC_tiedscalar))) {
            /* tied handle */
            writer->perl_obj = SvTIED_obj(MUTABLE_SV(io), mg);
        }
        else {
            /* simple handle */
            writer->perl_io = IoOFP(io);
        }
    }

    return writer;
}

INLINE void
XMLHash_writer_write(conv_writer_t *writer, const char *content, int len) {
    conv_buffer_t *buf = &writer->main_buf;

    if (len > (buf->end - buf->cur -1)) {
        XMLHash_writer_resize_buffer(writer, len + 1);
    }

    if (len < 17) {
        while (len--) {
            *buf->cur++ = *content++;
        }
    }
    else {
        memcpy(buf->cur, content, len);
        buf->cur += len;
    }
}

INLINE void
XMLHash_writer_write_quoted_string(conv_writer_t *writer, const char *content)
{
    char           ch;
    const char    *cur;
    int            len = 0;
    int            dq  = 0;
    int            sq  = 0;
    conv_buffer_t *buf = &writer->main_buf;

    cur = content;
    while ((ch = *cur++) != '\0') {
        len++;
        if (ch == '"') {
            dq++;
        }
        else if (ch == '\'') {
            sq++;
        }
    }

    if (len == 0) return;

    len *= 6;

    if (len > (buf->end - buf->cur -1)) {
        XMLHash_writer_resize_buffer(writer, len + 1);
    }

    if (dq) {
        if (sq) {
            *buf->cur++ = '"';
            while ((ch = *content++) != '\0') {
                if (ch == '"') {
                    *buf->cur++ = '&';
                    *buf->cur++ = 'q';
                    *buf->cur++ = 'u';
                    *buf->cur++ = 'o';
                    *buf->cur++ = 't';
                    *buf->cur++ = ';';
                }
                else {
                    *buf->cur++ = ch;
                }
            }
            *buf->cur++ = '"';
        }
        else {
            *buf->cur++ = '\'';
            while ((ch = *content++) != '\0') {
                *buf->cur++ = ch;
            }
            *buf->cur++ = '\'';
        }
    }
    else {
        *buf->cur++ = '"';
        while ((ch = *content++) != '\0') {
            *buf->cur++ = ch;
        }
        *buf->cur++ = '"';
    }
}

INLINE void
XMLHash_writer_escape_attr(conv_writer_t *writer, const char *content)
{
    char           ch;
    int            len = strlen(content) * 6;
    conv_buffer_t *buf = &writer->main_buf;

    if (len > (buf->end - buf->cur -1)) {
        XMLHash_writer_resize_buffer(writer, len + 1);
    }

    while ((ch = *content++) != 0) {
        switch (ch) {
            case '\n':
                *buf->cur++ = '&';
                *buf->cur++ = '#';
                *buf->cur++ = '1';
                *buf->cur++ = '0';
                *buf->cur++ = ';';
                break;
            case '\r':
                *buf->cur++ = '&';
                *buf->cur++ = '#';
                *buf->cur++ = '1';
                *buf->cur++ = '3';
                *buf->cur++ = ';';
                break;
            case '\t':
                *buf->cur++ = '&';
                *buf->cur++ = '#';
                *buf->cur++ = '9';
                *buf->cur++ = ';';
                break;
            case '<':
                *buf->cur++ = '&';
                *buf->cur++ = 'l';
                *buf->cur++ = 't';
                *buf->cur++ = ';';
                break;
            case '>':
                *buf->cur++ = '&';
                *buf->cur++ = 'g';
                *buf->cur++ = 't';
                *buf->cur++ = ';';
                break;
            case '&':
                *buf->cur++ = '&';
                *buf->cur++ = 'a';
                *buf->cur++ = 'm';
                *buf->cur++ = 'p';
                *buf->cur++ = ';';
                break;
            case '"':
                *buf->cur++ = '&';
                *buf->cur++ = 'q';
                *buf->cur++ = 'u';
                *buf->cur++ = 'o';
                *buf->cur++ = 't';
                *buf->cur++ = ';';
                break;
            default:
                *buf->cur++ = ch;
        }
    }
}

INLINE void
XMLHash_writer_escape_content(conv_writer_t *writer, const char *content, int len)
{
    char           ch;
    int            max_len;
    conv_buffer_t *buf = &writer->main_buf;

    if (len == -1) len = strlen(content);
    max_len = len * 5;

    if (max_len > (buf->end - buf->cur - 1)) {
        XMLHash_writer_resize_buffer(writer, max_len + 1);
    }

    while (len--) {
        ch = *content++;
        switch (ch) {
            case '\r':
                *buf->cur++ = '&';
                *buf->cur++ = '#';
                *buf->cur++ = '1';
                *buf->cur++ = '3';
                *buf->cur++ = ';';
                break;
            case '<':
                *buf->cur++ = '&';
                *buf->cur++ = 'l';
                *buf->cur++ = 't';
                *buf->cur++ = ';';
                break;
            case '>':
                *buf->cur++ = '&';
                *buf->cur++ = 'g';
                *buf->cur++ = 't';
                *buf->cur++ = ';';
                break;
            case '&':
                *buf->cur++ = '&';
                *buf->cur++ = 'a';
                *buf->cur++ = 'm';
                *buf->cur++ = 'p';
                *buf->cur++ = ';';
                break;
            default:
                *buf->cur++ = ch;
        }
    }
}

static int
cmpstringp(const void *p1, const void *p2)
{
    hash_entity_t *e1, *e2;
    e1 = (hash_entity_t *) p1;
    e2 = (hash_entity_t *) p2;
    return strcmp(e1->key, e2->key);
}

INLINE char *
XMLHash_trim_string(char *s, int *len)
{
    char *cur, *end, ch;
    int first = 1;

    end = cur = s;
    while ((ch = *cur++) != '\0') {
        switch (ch) {
            case ' ':
            case '\t':
            case '\n':
            case '\r':
                if (first) {
                    s = end = cur;
                }
                break;
            default:
                if (first) {
                    first--;
                }
                end = cur;
        }
    }

    *len = end - s;

    return s;
}

INLINE void
XMLHash_write_tag(convert_ctx_t *ctx, tagType type, char *name, int indent, int lf)
{
    int            indent_len;
    conv_writer_t *writer = ctx->writer;

    if (name == NULL) return;

    if (indent) {
        indent_len = ctx->indent_count * indent;
        if (indent_len > sizeof(indent_string))
            indent_len = sizeof(indent_string);

        BUFFER_WRITE(indent_string, indent_len);
    }

    if (type == TAG_CLOSE) {
        BUFFER_WRITE_CONSTANT("</");
    }
    else {
        BUFFER_WRITE_CONSTANT("<");
    }

    if (name[0] >= '1' && name[0] <= '9')
        BUFFER_WRITE_CONSTANT("_");

    BUFFER_WRITE_STRING(name, strlen(name));

    if (type == TAG_EMPTY) {
        BUFFER_WRITE_CONSTANT("/>");
    }
    else if (type == TAG_CLOSE || type == TAG_OPEN) {
        BUFFER_WRITE_CONSTANT(">");
    }

    if (lf)
        BUFFER_WRITE_CONSTANT("\n");
}

INLINE xmlNodePtr
XMLHash_write_tag2doc(char *name, char *content, xmlNodePtr rootNode)
{
    if (name == NULL) {
        return rootNode;
    }
    else if (name[0] >= '1' && name[0] <= '9') {
        int str_len = strlen(name);
        char *tmp = malloc(str_len + 1);
        if (tmp == NULL) {
            croak("Memory allocation error");
        }
        strcpy(&tmp[1], name);
        *tmp = '_';
        xmlNodePtr node = xmlNewChild(rootNode, NULL, BAD_CAST name, BAD_CAST content);
        free(tmp);
        return node;
    }

    return xmlNewChild(rootNode, NULL, BAD_CAST name, BAD_CAST content);
}

INLINE xmlNodePtr
XMLHash_write_tag2doc_escaped(char *name, char *content, xmlNodePtr rootNode)
{
    if (name == NULL) {
        return rootNode;
    }
    else if (name[0] >= '1' && name[0] <= '9') {
        int str_len = strlen(name);
        char *tmp = malloc(str_len + 1);
        if (tmp == NULL) {
            croak("Memory allocation error");
        }
        strcpy(&tmp[1], name);
        *tmp = '_';
        xmlNodePtr node = xmlNewTextChild(rootNode, NULL, BAD_CAST tmp, BAD_CAST content);
        free(tmp);
        return node;
    }

    return xmlNewTextChild(rootNode, NULL, BAD_CAST name, BAD_CAST content);
}

INLINE void
XMLHash_write_content(convert_ctx_t *ctx, char *value, int indent, int lf)
{
    int            indent_len, str_len;
    conv_writer_t *writer = ctx->writer;

    if (indent) {
        indent_len = ctx->indent_count * indent;
        if (indent_len > sizeof(indent_string))
            indent_len = sizeof(indent_string);

        BUFFER_WRITE(indent_string, indent_len);
    }

    if (ctx->opts.trim) {
        value = XMLHash_trim_string(value, &str_len);
        BUFFER_WRITE_ESCAPE(value, str_len);
    }
    else {
        BUFFER_WRITE_ESCAPE(value, -1);
    }

    if (lf)
        BUFFER_WRITE_CONSTANT("\n");
}

INLINE void
XMLHash_write_content2doc(convert_ctx_t *ctx, char *value, xmlNodePtr rootNode)
{
    int str_len;

    if (ctx->opts.trim) {
        value = XMLHash_trim_string(value, &str_len);
        xmlNodeAddContentLen(rootNode, BAD_CAST value, str_len);
    }
    else {
        xmlNodeAddContent(rootNode, BAD_CAST value);
    }
}

INLINE void
XMLHash_write_cdata(convert_ctx_t *ctx, char *value, int indent, int lf)
{
    int            indent_len, str_len;
    conv_writer_t *writer = ctx->writer;

    if (indent) {
        indent_len = ctx->indent_count * indent;
        if (indent_len > sizeof(indent_string))
            indent_len = sizeof(indent_string);

        BUFFER_WRITE(indent_string, indent_len);
    }

    BUFFER_WRITE_CONSTANT("<![CDATA[");
    if (ctx->opts.trim) {
        value = XMLHash_trim_string(value, &str_len);
        BUFFER_WRITE_STRING(value, str_len);
    }
    else {
        BUFFER_WRITE_STRING(value, strlen(value));
    }
    BUFFER_WRITE_CONSTANT("]]>");

    if (lf)
        BUFFER_WRITE_CONSTANT("\n");
}

INLINE void
XMLHash_write_cdata2doc(convert_ctx_t *ctx, char *value, xmlNodePtr rootNode)
{
    int str_len;

    if (ctx->opts.trim) {
        value = XMLHash_trim_string(value, &str_len);
        (void) xmlAddChild(rootNode, xmlNewCDataBlock(rootNode->doc, BAD_CAST value, str_len));
    }
    else {
        (void) xmlAddChild(rootNode, xmlNewCDataBlock(rootNode->doc, BAD_CAST value, strlen(value)));
    }
}

INLINE void
XMLHash_write_comment(convert_ctx_t *ctx, char *value, int indent, int lf)
{
    int            indent_len, str_len;
    conv_writer_t *writer = ctx->writer;

    if (indent) {
        indent_len = ctx->indent_count * indent;
        if (indent_len > sizeof(indent_string))
            indent_len = sizeof(indent_string);

        BUFFER_WRITE(indent_string, indent_len);
    }

    BUFFER_WRITE_CONSTANT("<!--");
    if (ctx->opts.trim) {
        value = XMLHash_trim_string(value, &str_len);
        BUFFER_WRITE_STRING(value, str_len);
    }
    else {
        BUFFER_WRITE_STRING(value, strlen(value));
    }
    BUFFER_WRITE_CONSTANT("-->");

    if (lf)
        BUFFER_WRITE_CONSTANT("\n");
}

INLINE void
XMLHash_write_comment2doc(convert_ctx_t *ctx, char *value, xmlNodePtr rootNode)
{
    int str_len;
    char ch;

    if (ctx->opts.trim) {
        value = XMLHash_trim_string(value, &str_len);
        ch = value[str_len];
        value[str_len] = '\0';
        (void) xmlAddChild(rootNode, xmlNewDocComment(rootNode->doc, BAD_CAST value));
        value[str_len] = ch;
    }
    else {
        (void) xmlAddChild(rootNode, xmlNewDocComment(rootNode->doc, BAD_CAST value));
    }
}

INLINE void
XMLHash_write_attribute_element(convert_ctx_t *ctx, char *name, char *value)
{
    if (name == NULL) return;

    conv_writer_t *writer = ctx->writer;

    BUFFER_WRITE_CONSTANT(" ");
    BUFFER_WRITE_STRING(name, strlen(name));
    BUFFER_WRITE_CONSTANT("=\"");
    BUFFER_WRITE_ESCAPE_ATTR(value);
    BUFFER_WRITE_CONSTANT("\"");
}

INLINE void
XMLHash_stash_push(stash_entity_t *stash, void *data)
{
    stash_entity_t *ent;
    ent = malloc(sizeof(stash_entity_t));
    if (ent == NULL)
        croak("Malloc error");

    ent->data   = data;
    ent->next   = stash->next;
    stash->next = ent;
}

void
XMLHash_stash_clean(stash_entity_t *stash)
{
    stash_entity_t *ent;

    while (stash->next != NULL) {
        ent = stash->next;
        SvREFCNT_dec((SV *)ent->data);
        stash->next = ent->next;
        free(ent);
    }
}

INLINE void
XMLHash_resolve_value(convert_ctx_t *ctx, SV **value, SV **value_ref, int *raw)
{
    int count;
    SV *sv;

    *raw = 0;

    while ( *value && SvROK(*value) ) {
        if (++ctx->recursion_depth > MAX_RECURSION_DEPTH)
            croak("Maximum recursion depth exceeded");

        *value_ref = *value;
        *value     = SvRV(*value);
        sv         = *value;

        if (expect_false( SvOBJECT(sv) )) {
            /* object */
            GV *to_string = gv_fetchmethod_autoload (SvSTASH (sv), "toString", 0);
            if (to_string) {
                dSP;

                ENTER; SAVETMPS; PUSHMARK (SP);
                XPUSHs (sv_bless (sv_2mortal (newRV_inc (sv)), SvSTASH (sv)));

                /* calling with G_SCALAR ensures that we always get a 1 return value */
                PUTBACK;
                call_sv ((SV *)GvCV (to_string), G_SCALAR);
                SPAGAIN;

                /* catch this surprisingly common error */
                if (SvROK (TOPs) && SvRV (TOPs) == sv)
                    croak("%s::toString method returned same object as was passed instead of a new one", HvNAME (SvSTASH (sv)));

                *value = POPs;
                PUTBACK;

                SvREFCNT_inc(*value);

                XMLHash_stash_push(&ctx->stash, *value);

                FREETMPS; LEAVE;

                *raw = 1;

                continue;
            }
        }
        else if(SvTYPE(*value) == SVt_PVCV) {
            /* code ref */
            *raw = 0;

            dSP;

            ENTER; SAVETMPS; PUSHMARK (SP);

            count = call_sv(*value, G_SCALAR|G_NOARGS);

            SPAGAIN;

            if (count == 1) {
                *value = POPs;

                SvREFCNT_inc(*value);

                XMLHash_stash_push(&ctx->stash, *value);

                PUTBACK;

                FREETMPS;
                LEAVE;

                continue;
            }
            else {
                *value = NULL;
            }
        }
    }
}

void
XMLHash_write_hash_no_attr(convert_ctx_t *ctx, char *name, SV *hash)
{
    SV   *value;
    HV   *hv;
    char *key;
    I32   keylen;
    int   i, len;

    if (!SvROK(hash)) {
        warn("parameter is not reference\n");
        return;
    }

    hv  = (HV *) SvRV(hash);
    len = HvUSEDKEYS(hv);

    if (len == 0) {
        XMLHash_write_tag(ctx, TAG_EMPTY, name, ctx->opts.indent, ctx->opts.indent);
        return;
    }

    XMLHash_write_tag(ctx, TAG_OPEN, name, ctx->opts.indent, ctx->opts.indent);

    ctx->indent_count++;

    hv_iterinit(hv);

    if (ctx->opts.canonical) {
        hash_entity_t a[len];

        i = 0;
        while ((value = hv_iternextsv(hv, &key, &keylen))) {
            a[i].value = value;
            a[i].key   = key;
            i++;
        }
        len = i;

        qsort(&a, len, sizeof(hash_entity_t), cmpstringp);

        for (i = 0; i < len; i++) {
            key   = a[i].key;
            value = a[i].value;
            XMLHash_write_item_no_attr(ctx, key, value);
        }
    }
    else {
        while ((value = hv_iternextsv(hv, &key, &keylen))) {
            XMLHash_write_item_no_attr(ctx, key, value);
        }
    }

    ctx->indent_count--;

    XMLHash_write_tag(ctx, TAG_CLOSE, name, ctx->opts.indent, ctx->opts.indent);
}

void
XMLHash_write_item_no_attr(convert_ctx_t *ctx, char *name, SV *value)
{
    I32            i, len;
    int            raw;
    SV            *value_ref = NULL;
    char          *str;
    STRLEN         str_len;
    conv_writer_t *writer = ctx->writer;

    XMLHash_resolve_value(ctx, &value, &value_ref, &raw);

    switch (SvTYPE(value)) {
        case SVt_NULL:
            XMLHash_write_tag(ctx, TAG_EMPTY, name, ctx->opts.indent, ctx->opts.indent);
            break;
        case SVt_IV:
        case SVt_PVIV:
        case SVt_PVNV:
        case SVt_NV:
        case SVt_PV:
            /* integer, double, scalar */
            XMLHash_write_tag(ctx, TAG_OPEN, name, ctx->opts.indent, 0);
            str = SvPV(value, str_len);
            if (raw) {
                BUFFER_WRITE_STRING(str, str_len);
            }
            else {
                BUFFER_WRITE_ESCAPE(str, str_len);
            }
            XMLHash_write_tag(ctx, TAG_CLOSE, name, 0, ctx->opts.indent);
            break;
        case SVt_PVAV:
            /* array */
            len = av_len((AV *) value);
            for (i = 0; i <= len; i++) {
                XMLHash_write_item_no_attr(ctx, name, *av_fetch((AV *) value, i, 0));
            }
            break;
        case SVt_PVHV:
            /* hash */
            XMLHash_write_hash_no_attr(ctx, name, value_ref);
            break;
        case SVt_PVMG:
            /* blessed */
            if (SvOK(value)) {
                str = SvPV(value, str_len);
                XMLHash_write_tag(ctx, TAG_OPEN, name, ctx->opts.indent, 0);
                if (raw) {
                    BUFFER_WRITE_STRING(str, str_len);
                }
                else {
                    BUFFER_WRITE_ESCAPE(str, str_len);
                }
                XMLHash_write_tag(ctx, TAG_CLOSE, name, 0, ctx->opts.indent);
                break;
            }
        default:
            XMLHash_write_tag(ctx, TAG_EMPTY, name, ctx->opts.indent, ctx->opts.indent);
    }

    ctx->recursion_depth--;
}

void
XMLHash_write_hash_no_attr2doc(convert_ctx_t *ctx, char *name, SV *hash, xmlNodePtr rootNode)
{
    SV   *value;
    HV   *hv;
    char *key;
    I32   keylen;
    int   i, len;

    if (!SvROK(hash)) {
        warn("parameter is not reference\n");
        return;
    }

    hv  = (HV *) SvRV(hash);
    len = HvUSEDKEYS(hv);

    rootNode = XMLHash_write_tag2doc(name, NULL, rootNode);

    if (len == 0) {
        return;
    }

    hv_iterinit(hv);

    if (ctx->opts.canonical) {
        hash_entity_t a[len];

        i = 0;
        while ((value = hv_iternextsv(hv, &key, &keylen))) {
            a[i].value = value;
            a[i].key   = key;
            i++;
        }
        len = i;

        qsort(&a, len, sizeof(hash_entity_t), cmpstringp);

        for (i = 0; i < len; i++) {
            key   = a[i].key;
            value = a[i].value;
            XMLHash_write_item_no_attr2doc(ctx, key, value, rootNode);
        }
    }
    else {
        while ((value = hv_iternextsv(hv, &key, &keylen))) {
            XMLHash_write_item_no_attr2doc(ctx, key, value, rootNode);
        }
    }
}

void
XMLHash_write_item_no_attr2doc(convert_ctx_t *ctx, char *name, SV *value, xmlNodePtr rootNode)
{
    I32        i, len;
    int        raw;
    SV        *value_ref = NULL;
    char      *str;
    STRLEN     str_len;

    XMLHash_resolve_value(ctx, &value, &value_ref, &raw);

    switch (SvTYPE(value)) {
        case SVt_NULL:
            (void) XMLHash_write_tag2doc(name, NULL, rootNode);
            break;
        case SVt_IV:
        case SVt_PVIV:
        case SVt_PVNV:
        case SVt_NV:
        case SVt_PV:
            /* integer, double, scalar */
            str = SvPV(value, str_len);
            if (raw) {
                (void) XMLHash_write_tag2doc(name, str, rootNode);
            }
            else {
                (void) XMLHash_write_tag2doc_escaped(name, str, rootNode);
            }
            break;
        case SVt_PVAV:
            /* array */
            len = av_len((AV *) value);
            for (i = 0; i <= len; i++) {
                XMLHash_write_item_no_attr2doc(ctx, name, *av_fetch((AV *) value, i, 0), rootNode);
            }
            break;
        case SVt_PVHV:
            /* hash */
            XMLHash_write_hash_no_attr2doc(ctx, name, value_ref, rootNode);
            break;
        case SVt_PVMG:
            /* blessed */
            if (SvOK(value)) {
                str = SvPV(value, str_len);
                if (raw) {
                    (void) XMLHash_write_tag2doc(name, str, rootNode);
                }
                else {
                    (void) XMLHash_write_tag2doc_escaped(name, str, rootNode);
                }
                break;
            }
        default:
            (void) XMLHash_write_tag2doc(name, NULL, rootNode);
    }

    ctx->recursion_depth--;
}

int
XMLHash_write_item(convert_ctx_t *ctx, char *name, SV *value, int flag)
{
    int            count = 0, raw = 0;
    I32            len, i;
    SV            *value_ref = NULL;
    conv_writer_t *writer = ctx->writer;

    if (ctx->opts.content[0] != '\0' && strcmp(name, ctx->opts.content) == 0) {
        flag = flag | FLAG_CONTENT;
    }

    XMLHash_resolve_value(ctx, &value, &value_ref, &raw);

    switch (SvTYPE(value)) {
        case SVt_NULL:
            if (flag & FLAG_SIMPLE && flag & FLAG_COMPLEX) {
                XMLHash_write_tag(ctx, TAG_EMPTY, name, ctx->opts.indent, ctx->opts.indent);
            }
            else if (flag & FLAG_SIMPLE && !(flag & FLAG_CONTENT)) {
                XMLHash_write_attribute_element(ctx, name, NULL);
                count++;
            }
            break;
        case SVt_IV:
        case SVt_PVIV:
        case SVt_PVNV:
        case SVt_NV:
        case SVt_PV:
            /* integer, double, scalar */
            if (flag & FLAG_SIMPLE && flag & FLAG_COMPLEX) {
                ctx->indent_count++;
                XMLHash_write_tag(ctx, TAG_OPEN, name, ctx->opts.indent, 0);
                BUFFER_WRITE_ESCAPE(SvPV_nolen(value), -1);
                XMLHash_write_tag(ctx, TAG_CLOSE, name, 0, ctx->opts.indent);
                ctx->indent_count--;
            }
            else if (flag & FLAG_COMPLEX && flag & FLAG_CONTENT) {
                ctx->indent_count++;
                XMLHash_write_content(ctx, SvPV_nolen(value), ctx->opts.indent, ctx->opts.indent);
                ctx->indent_count--;
            }
            else if (flag & FLAG_SIMPLE && !(flag & FLAG_CONTENT)) {
                XMLHash_write_attribute_element(ctx, name, (char *) SvPV_nolen(value));
                count++;
            }
            break;
        case SVt_PVAV:
            /* array */
            if (flag & FLAG_COMPLEX) {
                len = av_len((AV *) value);
                for (i = 0; i <= len; i++) {
                    XMLHash_write_item(ctx, name, *av_fetch((AV *) value, i, 0), FLAG_SIMPLE | FLAG_COMPLEX);
                }
                count++;
            }
            break;
        case SVt_PVHV:
            /* hash */
            if (flag & FLAG_COMPLEX) {
                ctx->indent_count++;
                XMLHash_write_hash(ctx, name, value_ref);
                ctx->indent_count--;
                count++;
            }
            break;
        case SVt_PVMG:
            /* blessed */
            if (SvOK(value)) {
                if (flag & FLAG_SIMPLE && flag & FLAG_COMPLEX) {
                    ctx->indent_count++;
                    XMLHash_write_tag(ctx, TAG_OPEN, name, ctx->opts.indent, 0);
                    BUFFER_WRITE_ESCAPE(SvPV_nolen(value), -1);
                    XMLHash_write_tag(ctx, TAG_CLOSE, name, 0, ctx->opts.indent);
                    ctx->indent_count--;
                }
                else if (flag & FLAG_COMPLEX && flag & FLAG_CONTENT) {
                    ctx->indent_count++;
                    XMLHash_write_content(ctx, SvPV_nolen(value), ctx->opts.indent, ctx->opts.indent);
                    ctx->indent_count--;
                }
                else if (flag & FLAG_SIMPLE && !(flag & FLAG_CONTENT)) {
                    XMLHash_write_attribute_element(ctx, name, (char *) SvPV_nolen(value));
                    count++;
                }
                break;
            }
        default:
            if (flag & FLAG_SIMPLE && !(flag & FLAG_CONTENT)) {
                XMLHash_write_attribute_element(ctx, name, NULL);
                count++;
            }
    }

    ctx->recursion_depth--;

    return count;
}

void
XMLHash_write_hash(convert_ctx_t *ctx, char *name, SV *hash)
{
    SV            *value;
    HV            *hv;
    char          *key;
    I32            keylen;
    int            i, done, len;
    conv_writer_t *writer = ctx->writer;

    if (!SvROK(hash)) {
        warn("parameter is not reference\n");
        return;
    }

    hv  = (HV *) SvRV(hash);
    len = HvUSEDKEYS(hv);

    if (len == 0) {
        XMLHash_write_tag(ctx, TAG_EMPTY, name, ctx->opts.indent, ctx->opts.indent);
        return;
    }

    XMLHash_write_tag(ctx, TAG_START, name, ctx->opts.indent, 0);

    hv_iterinit(hv);

    if (ctx->opts.canonical) {
        hash_entity_t a[len];

        i = 0;
        while ((value = hv_iternextsv(hv, &key, &keylen))) {
            a[i].value = value;
            a[i].key   = key;
            i++;
        }
        len = i;

        qsort(&a, len, sizeof(hash_entity_t), cmpstringp);

        done = 0;
        for (i = 0; i < len; i++) {
            key   = a[i].key;
            value = a[i].value;
            done += XMLHash_write_item(ctx, key, value, FLAG_SIMPLE);
        }

        if (done == len) {
            if (ctx->opts.indent) {
                BUFFER_WRITE_CONSTANT("/>\n");
            }
            else {
                BUFFER_WRITE_CONSTANT("/>");
            }
        }
        else {
            if (ctx->opts.indent) {
                BUFFER_WRITE_CONSTANT(">\n");
            }
            else {
                BUFFER_WRITE_CONSTANT(">");
            }

            for (i = 0; i < len; i++) {
                key   = a[i].key;
                value = a[i].value;
                XMLHash_write_item(ctx, key, value, FLAG_COMPLEX);
            }

            XMLHash_write_tag(ctx, TAG_CLOSE, name, ctx->opts.indent, ctx->opts.indent);
        }
    }
    else {
        done = 0;
        len  = 0;

        while ((value = hv_iternextsv(hv, &key, &keylen))) {
            done += XMLHash_write_item(ctx, key, value, FLAG_SIMPLE);
            len++;
        }

        if (done == len) {
            if (ctx->opts.indent) {
                BUFFER_WRITE_CONSTANT("/>\n");
            }
            else {
                BUFFER_WRITE_CONSTANT("/>");
            }
        }
        else {
            if (ctx->opts.indent) {
                BUFFER_WRITE_CONSTANT(">\n");
            }
            else {
                BUFFER_WRITE_CONSTANT(">");
            }

            while ((value = hv_iternextsv(hv, &key, &keylen))) {
                XMLHash_write_item(ctx, key, value, FLAG_COMPLEX);
            }


            XMLHash_write_tag(ctx, TAG_CLOSE, name, ctx->opts.indent, ctx->opts.indent);
        }
    }
}

int
XMLHash_write_item2doc(convert_ctx_t *ctx, char *name, SV *value, int flag, xmlNodePtr rootNode)
{
    int            count = 0, raw = 0;
    I32            len, i;
    SV            *value_ref = NULL;

    if (ctx->opts.content[0] != '\0' && strcmp(name, ctx->opts.content) == 0) {
        flag = flag | FLAG_CONTENT;
    }

    XMLHash_resolve_value(ctx, &value, &value_ref, &raw);

    switch (SvTYPE(value)) {
        case SVt_NULL:
            if (flag & FLAG_SIMPLE && flag & FLAG_COMPLEX) {
                (void) xmlNewChild(rootNode, NULL, BAD_CAST name, NULL);
            }
            else if (flag & FLAG_SIMPLE && !(flag & FLAG_CONTENT)) {
                xmlSetProp(rootNode, BAD_CAST name, NULL);
                count++;
            }
            break;
        case SVt_IV:
        case SVt_PVIV:
        case SVt_PVNV:
        case SVt_NV:
        case SVt_PV:
            /* integer, double, scalar */
            if (flag & FLAG_SIMPLE && flag & FLAG_COMPLEX) {
                (void) xmlNewTextChild(rootNode, NULL, BAD_CAST name, BAD_CAST SvPV_nolen(value));
            }
            else if (flag & FLAG_COMPLEX && flag & FLAG_CONTENT) {
                XMLHash_write_content2doc(ctx, SvPV_nolen(value), rootNode);
            }
            else if (flag & FLAG_SIMPLE && !(flag & FLAG_CONTENT)) {
                (void) xmlSetProp(rootNode, BAD_CAST name, BAD_CAST SvPV_nolen(value));
                count++;
            }
            break;
        case SVt_PVAV:
            /* array */
            if (flag & FLAG_COMPLEX) {
                len = av_len((AV *) value);
                for (i = 0; i <= len; i++) {
                    XMLHash_write_item2doc(ctx, name, *av_fetch((AV *) value, i, 0), FLAG_SIMPLE | FLAG_COMPLEX, rootNode);
                }
                count++;
            }
            break;
        case SVt_PVHV:
            /* hash */
            if (flag & FLAG_COMPLEX) {
                XMLHash_write_hash2doc(ctx, name, value_ref, rootNode);
                count++;
            }
            break;
        case SVt_PVMG:
            /* blessed */
            if (SvOK(value)) {
                if (flag & FLAG_SIMPLE && flag & FLAG_COMPLEX) {
                    (void) xmlNewTextChild(rootNode, NULL, BAD_CAST name, BAD_CAST SvPV_nolen(value));
                }
                else if (flag & FLAG_COMPLEX && flag & FLAG_CONTENT) {
                    XMLHash_write_content2doc(ctx, SvPV_nolen(value), rootNode);
                }
                else if (flag & FLAG_SIMPLE && !(flag & FLAG_CONTENT)) {
                    (void) xmlSetProp(rootNode, BAD_CAST name, BAD_CAST SvPV_nolen(value));
                    count++;
                }
                break;
            }
        default:
            if (flag & FLAG_SIMPLE && !(flag & FLAG_CONTENT)) {
                (void) xmlSetProp(rootNode, BAD_CAST name, NULL);
                count++;
            }
    }

    ctx->recursion_depth--;

    return count;
}

INLINE void
XMLHash_write_hash2doc(convert_ctx_t *ctx, char *name, SV *hash, xmlNodePtr rootNode)
{
    SV            *value;
    HV            *hv;
    char          *key;
    I32            keylen;
    int            i, done, len;

    if (!SvROK(hash)) {
        warn("parameter is not reference\n");
        return;
    }

    hv  = (HV *) SvRV(hash);
    len = HvUSEDKEYS(hv);

    if (len == 0) {
        (void) xmlNewChild(rootNode, NULL, BAD_CAST name, NULL);
        return;
    }

    rootNode = xmlNewChild(rootNode, NULL, BAD_CAST name, NULL);

    hv_iterinit(hv);

    if (ctx->opts.canonical) {
        hash_entity_t a[len];

        i = 0;
        while ((value = hv_iternextsv(hv, &key, &keylen))) {
            a[i].value = value;
            a[i].key   = key;
            i++;
        }
        len = i;

        qsort(&a, len, sizeof(hash_entity_t), cmpstringp);

        done = 0;
        for (i = 0; i < len; i++) {
            key   = a[i].key;
            value = a[i].value;
            done += XMLHash_write_item2doc(ctx, key, value, FLAG_SIMPLE, rootNode);
        }

        if (done != len) {
            for (i = 0; i < len; i++) {
                key   = a[i].key;
                value = a[i].value;
                XMLHash_write_item2doc(ctx, key, value, FLAG_COMPLEX, rootNode);
            }
        }
    }
    else {
        done = 0;
        len  = 0;

        while ((value = hv_iternextsv(hv, &key, &keylen))) {
            done += XMLHash_write_item2doc(ctx, key, value, FLAG_SIMPLE, rootNode);
            len++;
        }

        if (done != len) {
            while ((value = hv_iternextsv(hv, &key, &keylen))) {
                XMLHash_write_item2doc(ctx, key, value, FLAG_COMPLEX, rootNode);
            }
        }
    }
}

void
XMLHash_write_hash_lx(convert_ctx_t *ctx, SV *value, int flag)
{
    SV            *value_ref = NULL;
    SV            *hash_value, *hash_value_ref;
    HV            *hv;
    char          *key;
    I32            keylen;
    int            len, i, raw = 0;
    conv_writer_t *writer = ctx->writer;

    XMLHash_resolve_value(ctx, &value, &value_ref, &raw);

    switch (SvTYPE(value)) {
        case SVt_NULL:
            XMLHash_write_content(ctx, "", ctx->opts.indent, ctx->opts.indent);
            break;
        case SVt_IV:
        case SVt_PVIV:
        case SVt_PVNV:
        case SVt_NV:
        case SVt_PV:
            if (flag & FLAG_ATTR_ONLY) break;
            XMLHash_write_content(ctx, SvPV_nolen(value), ctx->opts.indent, ctx->opts.indent);
            break;
        case SVt_PVAV:
            len = av_len((AV *) value);
            for (i = 0; i <= len; i++) {
                XMLHash_write_hash_lx(ctx, *av_fetch((AV *) value, i, 0), flag);
            }
            break;
        case SVt_PVHV:
            hv  = (HV *) value;
            len = HvUSEDKEYS(hv);
            hv_iterinit(hv);

            while ((hash_value = hv_iternextsv(hv, &key, &keylen))) {
                if (ctx->opts.cdata[0] != '\0' && strcmp(key, ctx->opts.cdata) == 0) {
                    if (flag & FLAG_ATTR_ONLY) continue;
                    XMLHash_resolve_value(ctx, &hash_value, &hash_value_ref, &raw);
                    switch (SvTYPE(hash_value)) {
                        case SVt_NULL:
                            break;
                        case SVt_IV:
                        case SVt_PVIV:
                        case SVt_PVNV:
                        case SVt_NV:
                        case SVt_PV:
                            XMLHash_write_cdata(ctx, SvPV_nolen(hash_value), ctx->opts.indent, ctx->opts.indent);
                            break;
                        case SVt_PVAV:
                        case SVt_PVHV:
                            break;
                        case SVt_PVMG:
                            if (SvOK(value)) {
                                XMLHash_write_cdata(ctx, SvPV_nolen(hash_value), ctx->opts.indent, ctx->opts.indent);
                                break;
                            }
                        default:
                            XMLHash_write_cdata(ctx, SvPV_nolen(hash_value), ctx->opts.indent, ctx->opts.indent);
                    }
                }
                else if (ctx->opts.text[0] != '\0' && strcmp(key, ctx->opts.text) == 0) {
                    if (flag & FLAG_ATTR_ONLY) continue;
                    XMLHash_resolve_value(ctx, &hash_value, &hash_value_ref, &raw);
                    switch (SvTYPE(hash_value)) {
                        case SVt_NULL:
                            XMLHash_write_content(ctx, "", ctx->opts.indent, ctx->opts.indent);
                            break;
                        case SVt_IV:
                        case SVt_PVIV:
                        case SVt_PVNV:
                        case SVt_NV:
                        case SVt_PV:
                            XMLHash_write_content(ctx, SvPV_nolen(hash_value), ctx->opts.indent, ctx->opts.indent);
                            break;
                        case SVt_PVAV:
                        case SVt_PVHV:
                            break;
                        case SVt_PVMG:
                            if (SvOK(value)) {
                                XMLHash_write_content(ctx, SvPV_nolen(hash_value), ctx->opts.indent, ctx->opts.indent);
                                break;
                            }
                        default:
                            XMLHash_write_content(ctx, SvPV_nolen(hash_value), ctx->opts.indent, ctx->opts.indent);
                    }
                }
                else if (ctx->opts.comm[0] != '\0' && strcmp(key, ctx->opts.comm) == 0) {
                    if (flag & FLAG_ATTR_ONLY) continue;
                    XMLHash_resolve_value(ctx, &hash_value, &hash_value_ref, &raw);
                    switch (SvTYPE(hash_value)) {
                        case SVt_NULL:
                            XMLHash_write_comment(ctx, "", ctx->opts.indent, ctx->opts.indent);
                            break;
                        case SVt_IV:
                        case SVt_PVIV:
                        case SVt_PVNV:
                        case SVt_NV:
                        case SVt_PV:
                            XMLHash_write_comment(ctx, SvPV_nolen(hash_value), ctx->opts.indent, ctx->opts.indent);
                            break;
                        case SVt_PVAV:
                        case SVt_PVHV:
                            break;
                        case SVt_PVMG:
                            if (SvOK(value)) {
                                XMLHash_write_comment(ctx, SvPV_nolen(hash_value), ctx->opts.indent, ctx->opts.indent);
                                break;
                            }
                        default:
                            XMLHash_write_comment(ctx, SvPV_nolen(hash_value), ctx->opts.indent, ctx->opts.indent);
                    }
                }
                else if (ctx->opts.attr[0] != '\0') {
                    if (strncmp(key, ctx->opts.attr, ctx->opts.attr_len) == 0) {
                        if (!(flag & FLAG_ATTR_ONLY)) continue;
                        key += ctx->opts.attr_len;
                        XMLHash_resolve_value(ctx, &hash_value, &hash_value_ref, &raw);
                        switch (SvTYPE(hash_value)) {
                            case SVt_NULL:
                                XMLHash_write_attribute_element(ctx, key, (char *) "");
                                break;
                            case SVt_IV:
                            case SVt_PVIV:
                            case SVt_PVNV:
                            case SVt_NV:
                            case SVt_PV:
                                XMLHash_write_attribute_element(ctx, key, (char *) SvPV_nolen(hash_value));
                                break;
                            case SVt_PVAV:
                            case SVt_PVHV:
                                break;
                            case SVt_PVMG:
                                if (SvOK(value)) {
                                    XMLHash_write_attribute_element(ctx, key, (char *) SvPV_nolen(hash_value));
                                    break;
                                }
                            default:
                                XMLHash_write_attribute_element(ctx, key, (char *) SvPV_nolen(hash_value));
                        }
                    }
                    else {
                        if (flag & FLAG_ATTR_ONLY) continue;
                        if (SvTYPE(hash_value) == SVt_NULL) {
                            XMLHash_write_tag(ctx, TAG_EMPTY, key, ctx->opts.indent, ctx->opts.indent);
                        }
                        else {
                            XMLHash_write_tag(ctx, TAG_START, key, ctx->opts.indent, 0);
                            XMLHash_write_hash_lx(ctx, hash_value, FLAG_ATTR_ONLY);
                            if (ctx->opts.indent) {
                                BUFFER_WRITE_CONSTANT(">\n");
                            }
                            else {
                                BUFFER_WRITE_CONSTANT(">");
                            }
                            ctx->indent_count++;
                            XMLHash_write_hash_lx(ctx, hash_value, 0);
                            ctx->indent_count--;
                            XMLHash_write_tag(ctx, TAG_CLOSE, key, ctx->opts.indent, ctx->opts.indent);
                        }
                    }
                }
                else {
                    if (SvTYPE(hash_value) == SVt_NULL) {
                        XMLHash_write_tag(ctx, TAG_EMPTY, key, ctx->opts.indent, ctx->opts.indent);
                    }
                    else {
                        XMLHash_write_tag(ctx, TAG_OPEN, key, ctx->opts.indent, ctx->opts.indent);
                        ctx->indent_count++;
                        XMLHash_write_hash_lx(ctx, hash_value, 0);
                        ctx->indent_count--;
                        XMLHash_write_tag(ctx, TAG_CLOSE, key, ctx->opts.indent, ctx->opts.indent);
                    }
                }
            }

            break;
        case SVt_PVMG:
            /* blessed */
            if (flag & FLAG_ATTR_ONLY) break;
            if (SvOK(value)) {
                XMLHash_write_content(ctx, SvPV_nolen(value), ctx->opts.indent, ctx->opts.indent);
                break;
            }
        default:
            if (flag & FLAG_ATTR_ONLY) break;
            XMLHash_write_content(ctx, SvPV_nolen(value), ctx->opts.indent, ctx->opts.indent);
    }

    ctx->recursion_depth--;
}

void
XMLHash_write_hash_lx2doc(convert_ctx_t *ctx, SV *value, int flag, xmlNodePtr rootNode)
{
    SV            *value_ref = NULL;
    SV            *hash_value, *hash_value_ref;
    HV            *hv;
    char          *key;
    I32            keylen;
    int            len, i, raw = 0;
    xmlNodePtr     node;

    XMLHash_resolve_value(ctx, &value, &value_ref, &raw);

    switch (SvTYPE(value)) {
        case SVt_NULL:
            XMLHash_write_content2doc(ctx, "", rootNode);
            break;
        case SVt_IV:
        case SVt_PVIV:
        case SVt_PVNV:
        case SVt_NV:
        case SVt_PV:
            if (flag & FLAG_ATTR_ONLY) break;
            XMLHash_write_content2doc(ctx, SvPV_nolen(value), rootNode);
            break;
        case SVt_PVAV:
            len = av_len((AV *) value);
            for (i = 0; i <= len; i++) {
                XMLHash_write_hash_lx2doc(ctx, *av_fetch((AV *) value, i, 0), flag, rootNode);
            }
            break;
        case SVt_PVHV:
            hv  = (HV *) value;
            len = HvUSEDKEYS(hv);
            hv_iterinit(hv);

            while ((hash_value = hv_iternextsv(hv, &key, &keylen))) {
                if (ctx->opts.cdata[0] != '\0' && strcmp(key, ctx->opts.cdata) == 0) {
                    if (flag & FLAG_ATTR_ONLY) continue;
                    XMLHash_resolve_value(ctx, &hash_value, &hash_value_ref, &raw);
                    switch (SvTYPE(hash_value)) {
                        case SVt_NULL:
                            break;
                        case SVt_IV:
                        case SVt_PVIV:
                        case SVt_PVNV:
                        case SVt_NV:
                        case SVt_PV:
                            XMLHash_write_cdata2doc(ctx, SvPV_nolen(hash_value), rootNode);
                            break;
                        case SVt_PVAV:
                        case SVt_PVHV:
                            break;
                        case SVt_PVMG:
                            if (SvOK(value)) {
                                XMLHash_write_cdata2doc(ctx, SvPV_nolen(hash_value), rootNode);
                                break;
                            }
                        default:
                            XMLHash_write_cdata2doc(ctx, SvPV_nolen(hash_value), rootNode);
                    }
                }
                else if (ctx->opts.text[0] != '\0' && strcmp(key, ctx->opts.text) == 0) {
                    if (flag & FLAG_ATTR_ONLY) continue;
                    XMLHash_resolve_value(ctx, &hash_value, &hash_value_ref, &raw);
                    switch (SvTYPE(hash_value)) {
                        case SVt_NULL:
                            XMLHash_write_content2doc(ctx, "", rootNode);
                            break;
                        case SVt_IV:
                        case SVt_PVIV:
                        case SVt_PVNV:
                        case SVt_NV:
                        case SVt_PV:
                            XMLHash_write_content2doc(ctx, SvPV_nolen(hash_value), rootNode);
                            break;
                        case SVt_PVAV:
                        case SVt_PVHV:
                            break;
                        case SVt_PVMG:
                            if (SvOK(value)) {
                                XMLHash_write_content2doc(ctx, SvPV_nolen(hash_value), rootNode);
                                break;
                            }
                        default:
                            XMLHash_write_content2doc(ctx, SvPV_nolen(hash_value), rootNode);
                    }
                }
                else if (ctx->opts.comm[0] != '\0' && strcmp(key, ctx->opts.comm) == 0) {
                    if (flag & FLAG_ATTR_ONLY) continue;
                    XMLHash_resolve_value(ctx, &hash_value, &hash_value_ref, &raw);
                    switch (SvTYPE(hash_value)) {
                        case SVt_NULL:
                            XMLHash_write_comment2doc(ctx, "", rootNode);
                            break;
                        case SVt_IV:
                        case SVt_PVIV:
                        case SVt_PVNV:
                        case SVt_NV:
                        case SVt_PV:
                            XMLHash_write_comment2doc(ctx, SvPV_nolen(hash_value), rootNode);
                            break;
                        case SVt_PVAV:
                        case SVt_PVHV:
                            break;
                        case SVt_PVMG:
                            if (SvOK(value)) {
                                XMLHash_write_comment2doc(ctx, SvPV_nolen(hash_value), rootNode);
                                break;
                            }
                        default:
                            XMLHash_write_comment2doc(ctx, SvPV_nolen(hash_value), rootNode);
                    }
                }
                else if (ctx->opts.attr[0] != '\0') {
                    if (strncmp(key, ctx->opts.attr, ctx->opts.attr_len) == 0) {
                        if (!(flag & FLAG_ATTR_ONLY)) continue;
                        key += ctx->opts.attr_len;
                        XMLHash_resolve_value(ctx, &hash_value, &hash_value_ref, &raw);
                        switch (SvTYPE(hash_value)) {
                            case SVt_NULL:
                                xmlSetProp(rootNode, BAD_CAST key, BAD_CAST "");
                                break;
                            case SVt_IV:
                            case SVt_PVIV:
                            case SVt_PVNV:
                            case SVt_NV:
                            case SVt_PV:
                                xmlSetProp(rootNode, BAD_CAST key, BAD_CAST SvPV_nolen(hash_value));
                                break;
                            case SVt_PVAV:
                            case SVt_PVHV:
                                break;
                            case SVt_PVMG:
                                if (SvOK(value)) {
                                    xmlSetProp(rootNode, BAD_CAST key, BAD_CAST SvPV_nolen(hash_value));
                                    break;
                                }
                            default:
                                xmlSetProp(rootNode, BAD_CAST key, BAD_CAST SvPV_nolen(hash_value));
                        }
                    }
                    else {
                        if (flag & FLAG_ATTR_ONLY) continue;
                        if (SvTYPE(hash_value) == SVt_NULL) {
                            (void) xmlNewTextChild(rootNode, NULL, BAD_CAST key, BAD_CAST "");
                        }
                        else {
                            node = xmlNewTextChild(rootNode, NULL, BAD_CAST key, BAD_CAST "");
                            XMLHash_write_hash_lx2doc(ctx, hash_value, FLAG_ATTR_ONLY, node);
                            XMLHash_write_hash_lx2doc(ctx, hash_value, 0, node);
                        }
                    }
                }
                else {
                    if (SvTYPE(hash_value) == SVt_NULL) {
                        (void) xmlNewTextChild(rootNode, NULL, BAD_CAST key, BAD_CAST "");
                    }
                    else {
                        node = xmlNewTextChild(rootNode, NULL, BAD_CAST key, BAD_CAST "");
                        XMLHash_write_hash_lx2doc(ctx, hash_value, 0, node);
                    }
                }
            }

            break;
        case SVt_PVMG:
            /* blessed */
            if (flag & FLAG_ATTR_ONLY) break;
            if (SvOK(value)) {
                XMLHash_write_content2doc(ctx, SvPV_nolen(value), rootNode);
                break;
            }
        default:
            if (flag & FLAG_ATTR_ONLY) break;
            XMLHash_write_content2doc(ctx, SvPV_nolen(value), rootNode);
    }

    ctx->recursion_depth--;
}

void
XMLHash_conv_destroy(conv_opts_t *conv_opts)
{
    if (conv_opts != NULL) {
        free(conv_opts);
    }
}

bool_t
XMLHash_conv_init_options(conv_opts_t *opts)
{
    char   method[CONV_STR_PARAM_LEN];
    bool_t use_attr;

    CONV_READ_PARAM_INIT

    /* native options */
    CONV_READ_STRING_PARAM(opts->root,      "XML::Hash::XS::root",      CONV_DEF_ROOT);
    CONV_READ_STRING_PARAM(opts->version,   "XML::Hash::XS::version",   CONV_DEF_VERSION);
    CONV_READ_STRING_PARAM(opts->encoding,  "XML::Hash::XS::encoding",  CONV_DEF_ENCODING);
    CONV_READ_INT_PARAM   (opts->indent,    "XML::Hash::XS::indent",    CONV_DEF_INDENT);
    CONV_READ_BOOL_PARAM  (opts->canonical, "XML::Hash::XS::canonical", CONV_DEF_CANONICAL);
    CONV_READ_STRING_PARAM(opts->content,   "XML::Hash::XS::content",   CONV_DEF_CONTENT);
    CONV_READ_BOOL_PARAM  (opts->xml_decl,  "XML::Hash::XS::xml_decl",  CONV_DEF_XML_DECL);
    CONV_READ_BOOL_PARAM  (opts->doc,       "XML::Hash::XS::doc",       CONV_DEF_DOC);
    CONV_READ_BOOL_PARAM  (use_attr,        "XML::Hash::XS::use_attr",  CONV_DEF_USE_ATTR);

    /* XML::Hash::LX options */
    CONV_READ_STRING_PARAM(opts->attr,      "XML::Hash::XS::attr",      CONV_DEF_ATTR);
    opts->attr_len = strlen(opts->attr);
    CONV_READ_STRING_PARAM(opts->text,      "XML::Hash::XS::text",      CONV_DEF_TEXT);
    CONV_READ_BOOL_PARAM  (opts->trim,      "XML::Hash::XS::trim",      CONV_DEF_TRIM);
    CONV_READ_STRING_PARAM(opts->cdata,     "XML::Hash::XS::cdata",     CONV_DEF_CDATA);
    CONV_READ_STRING_PARAM(opts->comm,      "XML::Hash::XS::comm",      CONV_DEF_COMM);

    /* method */
    CONV_READ_STRING_PARAM(method,          "XML::Hash::XS::method",    CONV_DEF_METHOD);
    if (strcmp(method, "LX") == 0) {
        opts->method = CONV_METHOD_LX;
    }
    else if (use_attr) {
        opts->method = CONV_METHOD_NATIVE_ATTR_MODE;
    }
    else {
        opts->method = CONV_METHOD_NATIVE;
    }

    /* output, NULL - to string */
    CONV_READ_REF_PARAM   (opts->output,    "XML::Hash::XS::output",    CONV_DEF_OUTPUT);

    return TRUE;
}

conv_opts_t *
XMLHash_conv_create(void)
{
    conv_opts_t *conv_opts;

    if ((conv_opts = malloc(sizeof(conv_opts_t))) == NULL) {
        return NULL;
    }
    memset(conv_opts, 0, sizeof(conv_opts_t));

    if (! XMLHash_conv_init_options(conv_opts)) {
        XMLHash_conv_destroy(conv_opts);
        return NULL;
    }

    return conv_opts;
}

void
XMLHash_conv_assign_string_param(char param[], SV *value)
{
    char *str;

    if ( SvOK(value) ) {
        str = (char *) SvPV_nolen(value);
        strncpy(param, str, CONV_STR_PARAM_LEN);
    }
    else {
        *param = 0;
    }
}

void
XMLHash_conv_assign_int_param(char *name, int *param, SV *value)
{
    if ( !SvOK(value) ) {
        croak("Parameter '%s' is undefined", name);
    }
    *param = SvIV(value);
}

bool_t
XMLHash_conv_assign_bool_param(SV *value)
{
    if ( SvTRUE(value) ) {
        return TRUE;
    }
    return FALSE;
}

void
XMLHash_conv_parse_param(conv_opts_t *opts, int first, I32 ax, I32 items)
{
    if ((items - first) % 2 != 0) {
        croak("Odd number of parameters in new()");
    }

    int      i;
    char    *p, *cv;
    SV      *v;
    STRLEN   len;
    bool_t   use_attr = -1;

    for (i = first; i < items; i = i + 2) {
        v = ST(i);
        if (!SvOK(v)) {
            croak("Parameter name is undefined");
        }

        p = (char *) SvPV(v, len);
        v = ST(i + 1);

        switch (len) {
            case 3:
                if (str3cmp(p, 'd', 'o', 'c')) {
                    opts->doc = XMLHash_conv_assign_bool_param(v);
                    break;
                }
                goto error;
            case 4:
                if (str4cmp(p, 'a', 't', 't', 'r')) {
                    XMLHash_conv_assign_string_param(opts->attr, v);
                    if (opts->attr[0] == '\0') {
                        opts->attr_len = 0;
                    }
                    else {
                        opts->attr_len = strlen(opts->attr);
                    }
                    break;
                }
                if (str4cmp(p, 'c', 'o', 'm', 'm')) {
                    XMLHash_conv_assign_string_param(opts->comm, v);
                    break;
                }
                if (str4cmp(p, 'r', 'o', 'o', 't')) {
                    XMLHash_conv_assign_string_param(opts->root, v);
                    break;
                }
                if (str4cmp(p, 't', 'r', 'i', 'm')) {
                    opts->trim = XMLHash_conv_assign_bool_param(v);
                    break;
                }
                if (str4cmp(p, 't', 'e', 'x', 't')) {
                    XMLHash_conv_assign_string_param(opts->text, v);
                    break;
                }
                goto error;
            case 5:
                if (str5cmp(p, 'c', 'd', 'a', 't', 'a')) {
                    XMLHash_conv_assign_string_param(opts->cdata, v);
                    break;
                }
                goto error;
            case 6:
                if (str6cmp(p, 'i', 'n', 'd', 'e', 'n', 't')) {
                    XMLHash_conv_assign_int_param(p, &opts->indent, v);
                    break;
                }
                if (str6cmp(p, 'm', 'e', 't', 'h', 'o', 'd')) {
                    if (!SvOK(v)) {
                        croak("Parameter '%s' is undefined", p);
                    }
                    cv = SvPV(v, len);
                    switch  (len) {
                        case 6:
                            if (str6cmp(cv, 'N', 'A', 'T', 'I', 'V', 'E')) {
                                opts->method = CONV_METHOD_NATIVE;
                                break;
                            }
                            goto error_value;
                        case 2:
                            if (cv[0] == 'L' && cv[1] == 'X') {
                                opts->method = CONV_METHOD_LX;
                                break;
                            }
                            goto error_value;
                        default:
                            goto error_value;
                    }
                    break;
                }
                if (str6cmp(p, 'o', 'u', 't', 'p', 'u', 't')) {
                    if ( SvOK(v) && SvROK(v) ) {
                        opts->output = SvRV(v);
                    }
                    else {
                        opts->output = NULL;
                    }
                    break;
                }
                goto error;
            case 7:
                if (str7cmp(p, 'c', 'o', 'n', 't', 'e', 'n', 't')) {
                    XMLHash_conv_assign_string_param(opts->content, v);
                    break;
                }
                if (str7cmp(p, 'v', 'e', 'r', 's', 'i', 'o', 'n')) {
                    XMLHash_conv_assign_string_param(opts->version, v);
                    break;
                }
                goto error;
            case 8:
                if (str8cmp(p, 'e', 'n', 'c', 'o', 'd', 'i', 'n', 'g')) {
                    XMLHash_conv_assign_string_param(opts->encoding, v);
                    break;
                }
                if (str8cmp(p, 'u', 's', 'e', '_', 'a', 't', 't', 'r')) {
                    use_attr = XMLHash_conv_assign_bool_param(v);
                    break;
                }
                if (str8cmp(p, 'x', 'm', 'l', '_', 'd', 'e', 'c', 'l')) {
                    opts->xml_decl = XMLHash_conv_assign_bool_param(v);
                    break;
                }
                goto error;
            case 9:
                if (str9cmp(p, 'c', 'a', 'n', 'o', 'n', 'i', 'c', 'a', 'l')) {
                    opts->canonical = XMLHash_conv_assign_bool_param(v);
                    break;
                }
                goto error;
            default:
                goto error;
        }
    }

    if (use_attr != -1 && (opts->method == CONV_METHOD_NATIVE || opts->method == CONV_METHOD_NATIVE_ATTR_MODE)) {
        if (use_attr == TRUE) {
            opts->method = CONV_METHOD_NATIVE_ATTR_MODE;
        }
        else {
            opts->method = CONV_METHOD_NATIVE;
        }
    }

    return;

error_value:
    croak("Invalid parameter value for '%s': %s", p, cv);
    return;

error:
    croak("Invalid parameter '%s'", p);
}

SV *
XMLHash_hash2xml(convert_ctx_t *ctx, SV *hash)
{
    SV            *result;
    conv_writer_t *writer = NULL;

    /* run */
    dXCPT;
    XCPT_TRY_START
    {
        ctx->writer = writer = XMLHash_writer_create(&ctx->opts, 16384);

        if (ctx->opts.xml_decl) {
            /* xml declaration */
            BUFFER_WRITE_CONSTANT("<?xml version=");
            BUFFER_WRITE_QUOTED(ctx->opts.version);
            BUFFER_WRITE_CONSTANT(" encoding=");
            BUFFER_WRITE_QUOTED(ctx->opts.encoding);
            BUFFER_WRITE_CONSTANT("?>\n");
        }

        switch (ctx->opts.method) {
            case CONV_METHOD_NATIVE:
                ctx->opts.trim = 0;
                XMLHash_write_hash_no_attr(ctx, ctx->opts.root, hash);
                break;
            case CONV_METHOD_NATIVE_ATTR_MODE:
                ctx->opts.trim = 0;
                XMLHash_write_hash(ctx, ctx->opts.root, hash);
                break;
            case CONV_METHOD_LX:
                XMLHash_write_hash_lx(ctx, hash, 0);
                break;
            default:
                croak("Invalid method");
        }
    } XCPT_TRY_END

    XCPT_CATCH
    {
        XMLHash_stash_clean(&ctx->stash);
        XMLHash_writer_destroy(writer);
        XCPT_RETHROW;
    }

    XMLHash_stash_clean(&ctx->stash);
    result = XMLHash_writer_flush(writer);
    XMLHash_writer_destroy(writer);

    return result;
}

SV *
XMLHash_hash2dom(convert_ctx_t *ctx, SV *hash)
{
    xmlDocPtr doc = xmlNewDoc(BAD_CAST ctx->opts.version);
    if (doc == NULL) {
        croak("Can't create new document");
    }
    doc->encoding = (const xmlChar*) xmlStrdup((const xmlChar*) ctx->opts.encoding);

    dXCPT;
    XCPT_TRY_START
    {
        switch (ctx->opts.method) {
            case CONV_METHOD_NATIVE:
                ctx->opts.trim = 0;
                XMLHash_write_hash_no_attr2doc(ctx, ctx->opts.root, hash, (xmlNodePtr) doc);
                break;
            case CONV_METHOD_NATIVE_ATTR_MODE:
                ctx->opts.trim = 0;
                XMLHash_write_hash2doc(ctx, ctx->opts.root, hash, (xmlNodePtr) doc);
                break;
            case CONV_METHOD_LX:
                XMLHash_write_hash_lx2doc(ctx, hash, 0, (xmlNodePtr) doc);
                break;
            default:
                croak("Invalid method");
        }
    } XCPT_TRY_END

    XCPT_CATCH
    {
        XMLHash_stash_clean(&ctx->stash);
        XCPT_RETHROW;
    }

    XMLHash_stash_clean(&ctx->stash);

    return x_PmmNodeToSv((xmlNodePtr) doc, NULL);
}

MODULE = XML::Hash::XS PACKAGE = XML::Hash::XS

PROTOTYPES: DISABLE

conv_opts_t *
new(CLASS,...)
    PREINIT:
        conv_opts_t  *conv_opts;
    CODE:
        if ((conv_opts = XMLHash_conv_create()) == NULL) {
            croak("Malloc error in new()");
        }

        dXCPT;
        XCPT_TRY_START
        {
            XMLHash_conv_parse_param(conv_opts, 1, ax, items);
        } XCPT_TRY_END

        XCPT_CATCH
        {
            XMLHash_conv_destroy(conv_opts);
            XCPT_RETHROW;
        }

        RETVAL = conv_opts;
    OUTPUT:
        RETVAL

SV *
hash2xml(...)
    PREINIT:
        conv_opts_t   *conv_opts = NULL;
        convert_ctx_t  ctx;
        SV            *p, *hash, *result;
        int            nparam    = 0;
    CODE:
        /* get object reference */
        if (nparam >= items)
            croak("Invalid parameters");

        p = ST(nparam);
        if ( sv_isa(p, "XML::Hash::XS") ) {
            /* reference to object */
            IV tmp = SvIV((SV *) SvRV(p));
            conv_opts = INT2PTR(conv_opts_t *, tmp);
            nparam++;
        }
        else if ( SvTYPE(p) == SVt_PV ) {
            /* class name */
            nparam++;
        }

        /* get hash reference */
        if (nparam >= items)
            croak("Invalid parameters");

        p = ST(nparam);
        if (SvROK(p) && SvTYPE(SvRV(p)) == SVt_PVHV) {
            hash = p;
            nparam++;
        }
        else {
            croak("Parameter is not hash reference");
        }

        /* set options */
        memset(&ctx, 0, sizeof(convert_ctx_t));
        if (conv_opts == NULL) {
            /* read global options */
            XMLHash_conv_init_options(&ctx.opts);
        }
        else {
            /* read options from object */
            memcpy(&ctx.opts, conv_opts, sizeof(conv_opts_t));
        }
        if (nparam < items) {
            XMLHash_conv_parse_param(&ctx.opts, nparam, ax, items);
        }

        /* run */
        if (ctx.opts.doc) {
            result = XMLHash_hash2dom(&ctx, hash);
        }
        else {
            result = XMLHash_hash2xml(&ctx, hash);
        }

        if (ctx.opts.output != NULL) {
            XSRETURN_UNDEF;
        }

        if (result == NULL) {
            warn("Failed to convert");
            XSRETURN_UNDEF;
        }

        RETVAL = result;

    OUTPUT:
        RETVAL

void
DESTROY(conv)
        conv_opts_t *conv;
    CODE:
        XMLHash_conv_destroy(conv);