/*
 * Copyright (c) 2009, Natacha Porté
 * Copyright (c) 2011, Vicent Marti
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "markdown.h"
#include "html.h"

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

#include "houdini.h"

#define USE_XHTML( opt ) (opt->flags & HTML_USE_XHTML)

int sdhtml_is_tag( const uint8_t* tag_data, size_t tag_size, const char* tagname )
{
    size_t i;
    int closed = 0;

    if( tag_size < 3 || tag_data[0] != '<' )
        return HTML_TAG_NONE;

    i = 1;

    if( tag_data[i] == '/' )
    {
        closed = 1;
        i++;
    }

    for( ; i < tag_size; ++i, ++tagname )
    {
        if( *tagname == 0 )
            break;

        if( tag_data[i] != *tagname )
            return HTML_TAG_NONE;
    }

    if( i == tag_size )
        return HTML_TAG_NONE;

    if( isspace( tag_data[i] ) || tag_data[i] == '>' )
        return closed ? HTML_TAG_CLOSE : HTML_TAG_OPEN;

    return HTML_TAG_NONE;
}


static inline void escape_html( struct buf* ob, const uint8_t* source, size_t length )
{
    houdini_escape_html0( ob, source, length, 0 );
}


static inline void escape_href( struct buf* ob, const uint8_t* source, size_t length )
{
    houdini_escape_href( ob, source, length );
}


/********************
* GENERIC RENDERER *
********************/
static int rndr_autolink( struct buf* ob,
        const struct buf* link,
        enum mkd_autolink type,
        void* opaque )
{
    struct html_renderopt* options = opaque;

    if( !link || !link->size )
        return 0;

    if( (options->flags & HTML_SAFELINK) != 0
        && !sd_autolink_issafe( link->data, link->size )
        && type != MKDA_EMAIL )
        return 0;

    BUFPUTSL( ob, "<a href=\"" );

    if( type == MKDA_EMAIL )
        BUFPUTSL( ob, "mailto:" );

    escape_href( ob, link->data, link->size );

    if( options->link_attributes )
    {
        bufputc( ob, '\"' );
        options->link_attributes( ob, link, opaque );
        bufputc( ob, '>' );
    }
    else
    {
        BUFPUTSL( ob, "\">" );
    }

    /*
     * Pretty printing: if we get an email address as
     * an actual URI, e.g. `mailto:foo@bar.com`, we don't
     * want to print the `mailto:` prefix
     */
    if( bufprefix( link, "mailto:" ) == 0 )
    {
        escape_html( ob, link->data + 7, link->size - 7 );
    }
    else
    {
        escape_html( ob, link->data, link->size );
    }

    BUFPUTSL( ob, "</a>" );

    return 1;
}


static void rndr_blockcode( struct buf* ob,
        const struct buf* text,
        const struct buf* lang,
        void* opaque )
{
    if( ob->size )
        bufputc( ob, '\n' );

    if( lang && lang->size )
    {
        size_t i, cls;
        BUFPUTSL( ob, "<pre><code class=\"" );

        for( i = 0, cls = 0; i < lang->size; ++i, ++cls )
        {
            while( i < lang->size && isspace( lang->data[i] ) )
                i++;

            if( i < lang->size )
            {
                size_t org = i;

                while( i < lang->size && !isspace( lang->data[i] ) )
                    i++;

                if( lang->data[org] == '.' )
                    org++;

                if( cls )
                    bufputc( ob, ' ' );

                escape_html( ob, lang->data + org, i - org );
            }
        }

        BUFPUTSL( ob, "\">" );
    }
    else
        BUFPUTSL( ob, "<pre><code>" );

    if( text )
        escape_html( ob, text->data, text->size );

    BUFPUTSL( ob, "</code></pre>\n" );
}


static void rndr_blockquote( struct buf* ob, const struct buf* text, void* opaque )
{
    if( ob->size )
        bufputc( ob, '\n' );

    BUFPUTSL( ob, "<blockquote>\n" );

    if( text )
        bufput( ob, text->data, text->size );

    BUFPUTSL( ob, "</blockquote>\n" );
}


static int rndr_codespan( struct buf* ob, const struct buf* text, void* opaque )
{
    BUFPUTSL( ob, "<code>" );

    if( text )
        escape_html( ob, text->data, text->size );

    BUFPUTSL( ob, "</code>" );
    return 1;
}


static int rndr_strikethrough( struct buf* ob, const struct buf* text, void* opaque )
{
    if( !text || !text->size )
        return 0;

    BUFPUTSL( ob, "<del>" );
    bufput( ob, text->data, text->size );
    BUFPUTSL( ob, "</del>" );
    return 1;
}


static int rndr_double_emphasis( struct buf* ob, const struct buf* text, void* opaque )
{
    if( !text || !text->size )
        return 0;

    BUFPUTSL( ob, "<strong>" );
    bufput( ob, text->data, text->size );
    BUFPUTSL( ob, "</strong>" );

    return 1;
}


static int rndr_emphasis( struct buf* ob, const struct buf* text, void* opaque )
{
    if( !text || !text->size )
        return 0;

    BUFPUTSL( ob, "<em>" );

    if( text )
        bufput( ob, text->data, text->size );

    BUFPUTSL( ob, "</em>" );
    return 1;
}


static int rndr_linebreak( struct buf* ob, void* opaque )
{
    struct html_renderopt* options = opaque;

    bufputs( ob, USE_XHTML( options ) ? "<br/>\n" : "<br>\n" );
    return 1;
}


static void rndr_header( struct buf* ob, const struct buf* text, int level, void* opaque )
{
    struct html_renderopt* options = opaque;

    if( ob->size )
        bufputc( ob, '\n' );

    if( options->flags & HTML_TOC )
        bufprintf( ob, "<h%d id=\"toc_%d\">", level, options->toc_data.header_count++ );
    else
        bufprintf( ob, "<h%d>", level );

    if( text )
        bufput( ob, text->data, text->size );

    bufprintf( ob, "</h%d>\n", level );
}


static int rndr_link( struct buf* ob,
        const struct buf* link,
        const struct buf* title,
        const struct buf* content,
        void* opaque )
{
    struct html_renderopt* options = opaque;

    if( link != NULL && (options->flags & HTML_SAFELINK) != 0
        && !sd_autolink_issafe( link->data, link->size ) )
        return 0;

    BUFPUTSL( ob, "<a href=\"" );

    if( link && link->size )
        escape_href( ob, link->data, link->size );

    if( title && title->size )
    {
        BUFPUTSL( ob, "\" title=\"" );
        escape_html( ob, title->data, title->size );
    }

    if( options->link_attributes )
    {
        bufputc( ob, '\"' );
        options->link_attributes( ob, link, opaque );
        bufputc( ob, '>' );
    }
    else
    {
        BUFPUTSL( ob, "\">" );
    }

    if( content && content->size )
        bufput( ob, content->data, content->size );

    BUFPUTSL( ob, "</a>" );
    return 1;
}


static void rndr_list( struct buf* ob, const struct buf* text, int flags, void* opaque )
{
    if( ob->size )
        bufputc( ob, '\n' );

    bufput( ob, flags & MKD_LIST_ORDERED ? "<ol>\n" : "<ul>\n", 5 );

    if( text )
        bufput( ob, text->data, text->size );

    bufput( ob, flags & MKD_LIST_ORDERED ? "</ol>\n" : "</ul>\n", 6 );
}


static void rndr_listitem( struct buf* ob, const struct buf* text, int flags, void* opaque )
{
    BUFPUTSL( ob, "<li>" );

    if( text )
    {
        size_t size = text->size;

        while( size && text->data[size - 1] == '\n' )
            size--;

        bufput( ob, text->data, size );
    }

    BUFPUTSL( ob, "</li>\n" );
}


static void rndr_paragraph( struct buf* ob, const struct buf* text, void* opaque )
{
    struct html_renderopt* options = opaque;
    size_t i = 0;

    if( ob->size )
        bufputc( ob, '\n' );

    if( !text || !text->size )
        return;

    while( i < text->size && isspace( text->data[i] ) )
        i++;

    if( i == text->size )
        return;

    BUFPUTSL( ob, "<p>" );

    if( options->flags & HTML_HARD_WRAP )
    {
        size_t org;

        while( i < text->size )
        {
            org = i;

            while( i < text->size && text->data[i] != '\n' )
                i++;

            if( i > org )
                bufput( ob, text->data + org, i - org );

            /*
             * do not insert a line break if this newline
             * is the last character on the paragraph
             */
            if( i >= text->size - 1 )
                break;

            rndr_linebreak( ob, opaque );
            i++;
        }
    }
    else
    {
        bufput( ob, &text->data[i], text->size - i );
    }

    BUFPUTSL( ob, "</p>\n" );
}


static void rndr_raw_block( struct buf* ob, const struct buf* text, void* opaque )
{
    size_t org, sz;

    if( !text )
        return;

    sz = text->size;

    while( sz > 0 && text->data[sz - 1] == '\n' )
        sz--;

    org = 0;

    while( org < sz && text->data[org] == '\n' )
        org++;

    if( org >= sz )
        return;

    if( ob->size )
        bufputc( ob, '\n' );

    bufput( ob, text->data + org, sz - org );
    bufputc( ob, '\n' );
}


static int rndr_triple_emphasis( struct buf* ob, const struct buf* text, void* opaque )
{
    if( !text || !text->size )
        return 0;

    BUFPUTSL( ob, "<strong><em>" );
    bufput( ob, text->data, text->size );
    BUFPUTSL( ob, "</em></strong>" );
    return 1;
}


static void rndr_hrule( struct buf* ob, void* opaque )
{
    struct html_renderopt* options = opaque;

    if( ob->size )
        bufputc( ob, '\n' );

    bufputs( ob, USE_XHTML( options ) ? "<hr/>\n" : "<hr>\n" );
}


static int rndr_image( struct buf* ob,
        const struct buf* link,
        const struct buf* title,
        const struct buf* alt,
        void* opaque )
{
    struct html_renderopt* options = opaque;

    if( !link || !link->size )
        return 0;

    BUFPUTSL( ob, "<img src=\"" );
    escape_href( ob, link->data, link->size );
    BUFPUTSL( ob, "\" alt=\"" );

    if( alt && alt->size )
        escape_html( ob, alt->data, alt->size );

    if( title && title->size )
    {
        BUFPUTSL( ob, "\" title=\"" );
        escape_html( ob, title->data, title->size );
    }

    bufputs( ob, USE_XHTML( options ) ? "\"/>" : "\">" );
    return 1;
}


static int rndr_raw_html( struct buf* ob, const struct buf* text, void* opaque )
{
    struct html_renderopt* options = opaque;

    /* HTML_ESCAPE overrides SKIP_HTML, SKIP_STYLE, SKIP_LINKS and SKIP_IMAGES
     * It doens't see if there are any valid tags, just escape all of them. */
    if( (options->flags & HTML_ESCAPE) != 0 )
    {
        escape_html( ob, text->data, text->size );
        return 1;
    }

    if( (options->flags & HTML_SKIP_HTML) != 0 )
        return 1;

    if( (options->flags & HTML_SKIP_STYLE) != 0
        && sdhtml_is_tag( text->data, text->size, "style" ) )
        return 1;

    if( (options->flags & HTML_SKIP_LINKS) != 0
        && sdhtml_is_tag( text->data, text->size, "a" ) )
        return 1;

    if( (options->flags & HTML_SKIP_IMAGES) != 0
        && sdhtml_is_tag( text->data, text->size, "img" ) )
        return 1;

    bufput( ob, text->data, text->size );
    return 1;
}


static void rndr_table( struct buf* ob,
        const struct buf* header,
        const struct buf* body,
        void* opaque )
{
    if( ob->size )
        bufputc( ob, '\n' );

    BUFPUTSL( ob, "<table><thead>\n" );

    if( header )
        bufput( ob, header->data, header->size );

    BUFPUTSL( ob, "</thead><tbody>\n" );

    if( body )
        bufput( ob, body->data, body->size );

    BUFPUTSL( ob, "</tbody></table>\n" );
}


static void rndr_tablerow( struct buf* ob, const struct buf* text, void* opaque )
{
    BUFPUTSL( ob, "<tr>\n" );

    if( text )
        bufput( ob, text->data, text->size );

    BUFPUTSL( ob, "</tr>\n" );
}


static void rndr_tablecell( struct buf* ob, const struct buf* text, int flags, void* opaque )
{
    if( flags & MKD_TABLE_HEADER )
    {
        BUFPUTSL( ob, "<th" );
    }
    else
    {
        BUFPUTSL( ob, "<td" );
    }

    switch( flags & MKD_TABLE_ALIGNMASK )
    {
    case MKD_TABLE_ALIGN_CENTER:
        BUFPUTSL( ob, " align=\"center\">" );
        break;

    case MKD_TABLE_ALIGN_L:
        BUFPUTSL( ob, " align=\"left\">" );
        break;

    case MKD_TABLE_ALIGN_R:
        BUFPUTSL( ob, " align=\"right\">" );
        break;

    default:
        BUFPUTSL( ob, ">" );
    }

    if( text )
        bufput( ob, text->data, text->size );

    if( flags & MKD_TABLE_HEADER )
    {
        BUFPUTSL( ob, "</th>\n" );
    }
    else
    {
        BUFPUTSL( ob, "</td>\n" );
    }
}


static int rndr_superscript( struct buf* ob, const struct buf* text, void* opaque )
{
    if( !text || !text->size )
        return 0;

    BUFPUTSL( ob, "<sup>" );
    bufput( ob, text->data, text->size );
    BUFPUTSL( ob, "</sup>" );
    return 1;
}


static void rndr_normal_text( struct buf* ob, const struct buf* text, void* opaque )
{
    if( text )
        escape_html( ob, text->data, text->size );
}


static void toc_header( struct buf* ob, const struct buf* text, int level, void* opaque )
{
    struct html_renderopt* options = opaque;

    /* set the level offset if this is the first header
     * we're parsing for the document */
    if( options->toc_data.current_level == 0 )
    {
        options->toc_data.level_offset = level - 1;
    }

    level -= options->toc_data.level_offset;

    if( level > options->toc_data.current_level )
    {
        while( level > options->toc_data.current_level )
        {
            BUFPUTSL( ob, "<ul>\n<li>\n" );
            options->toc_data.current_level++;
        }
    }
    else if( level < options->toc_data.current_level )
    {
        BUFPUTSL( ob, "</li>\n" );

        while( level < options->toc_data.current_level )
        {
            BUFPUTSL( ob, "</ul>\n</li>\n" );
            options->toc_data.current_level--;
        }

        BUFPUTSL( ob, "<li>\n" );
    }
    else
    {
        BUFPUTSL( ob, "</li>\n<li>\n" );
    }

    bufprintf( ob, "<a href=\"#toc_%d\">", options->toc_data.header_count++ );

    if( text )
        escape_html( ob, text->data, text->size );

    BUFPUTSL( ob, "</a>\n" );
}


static int toc_link( struct buf* ob,
        const struct buf* link,
        const struct buf* title,
        const struct buf* content,
        void* opaque )
{
    if( content && content->size )
        bufput( ob, content->data, content->size );

    return 1;
}


static void toc_finalize( struct buf* ob, void* opaque )
{
    struct html_renderopt* options = opaque;

    while( options->toc_data.current_level > 0 )
    {
        BUFPUTSL( ob, "</li>\n</ul>\n" );
        options->toc_data.current_level--;
    }
}


void sdhtml_toc_renderer( struct sd_callbacks* callbacks, struct html_renderopt* options )
{
    static const struct sd_callbacks cb_default =
    {
        NULL,
        NULL,
        NULL,
        toc_header,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,

        NULL,
        rndr_codespan,
        rndr_double_emphasis,
        rndr_emphasis,
        NULL,
        NULL,
        toc_link,
        NULL,
        rndr_triple_emphasis,
        rndr_strikethrough,
        rndr_superscript,

        NULL,
        NULL,

        NULL,
        toc_finalize,
    };

    memset( options, 0x0, sizeof(struct html_renderopt) );
    options->flags = HTML_TOC;

    memcpy( callbacks, &cb_default, sizeof(struct sd_callbacks) );
}


void sdhtml_renderer( struct sd_callbacks* callbacks,
        struct html_renderopt* options,
        unsigned int render_flags )
{
    static const struct sd_callbacks cb_default =
    {
        rndr_blockcode,
        rndr_blockquote,
        rndr_raw_block,
        rndr_header,
        rndr_hrule,
        rndr_list,
        rndr_listitem,
        rndr_paragraph,
        rndr_table,
        rndr_tablerow,
        rndr_tablecell,

        rndr_autolink,
        rndr_codespan,
        rndr_double_emphasis,
        rndr_emphasis,
        rndr_image,
        rndr_linebreak,
        rndr_link,
        rndr_raw_html,
        rndr_triple_emphasis,
        rndr_strikethrough,
        rndr_superscript,

        NULL,
        rndr_normal_text,

        NULL,
        NULL,
    };

    /* Prepare the options pointer */
    memset( options, 0x0, sizeof(struct html_renderopt) );
    options->flags = render_flags;

    /* Prepare the callbacks */
    memcpy( callbacks, &cb_default, sizeof(struct sd_callbacks) );

    if( render_flags & HTML_SKIP_IMAGES )
        callbacks->image = NULL;

    if( render_flags & HTML_SKIP_LINKS )
    {
        callbacks->link = NULL;
        callbacks->autolink = NULL;
    }

    if( render_flags & HTML_SKIP_HTML || render_flags & HTML_ESCAPE )
        callbacks->blockhtml = NULL;
}
