/*
 * Copyright (c) Kristaps Dzonsons <kristaps@bsd.lv>
 *
 * 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 "config.h"

#if HAVE_SYS_QUEUE
# include <sys/queue.h>
#endif

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

#include "lowdown.h"
#include "extern.h"

static	const char *const names[LOWDOWN__MAX] = {
	"LOWDOWN_ROOT",			/* LOWDOWN_ROOT */
	"LOWDOWN_BLOCKCODE",            /* LOWDOWN_BLOCKCODE */
	"LOWDOWN_BLOCKQUOTE",           /* LOWDOWN_BLOCKQUOTE */
	"LOWDOWN_DEFINITION",		/* LOWDOWN_DEFINITION */
	"LOWDOWN_DEFINITION_TITLE",	/* LOWDOWN_DEFINITION_TITLE */
	"LOWDOWN_DEFINITION_DATA",	/* LOWDOWN_DEFINITION_DATA */
	"LOWDOWN_HEADER",               /* LOWDOWN_HEADER */
	"LOWDOWN_HRULE",                /* LOWDOWN_HRULE */
	"LOWDOWN_LIST",                 /* LOWDOWN_LIST */
	"LOWDOWN_LISTITEM",             /* LOWDOWN_LISTITEM */
	"LOWDOWN_PARAGRAPH",            /* LOWDOWN_PARAGRAPH */
	"LOWDOWN_TABLE_BLOCK",          /* LOWDOWN_TABLE_BLOCK */
	"LOWDOWN_TABLE_HEADER",         /* LOWDOWN_TABLE_HEADER */
	"LOWDOWN_TABLE_BODY",           /* LOWDOWN_TABLE_BODY */
	"LOWDOWN_TABLE_ROW",            /* LOWDOWN_TABLE_ROW */
	"LOWDOWN_TABLE_CELL",           /* LOWDOWN_TABLE_CELL */
	"LOWDOWN_BLOCKHTML",            /* LOWDOWN_BLOCKHTML */
	"LOWDOWN_LINK_AUTO",            /* LOWDOWN_LINK_AUTO */
	"LOWDOWN_CODESPAN",             /* LOWDOWN_CODESPAN */
	"LOWDOWN_DOUBLE_EMPHASIS",      /* LOWDOWN_DOUBLE_EMPHASIS */
	"LOWDOWN_EMPHASIS",             /* LOWDOWN_EMPHASIS */
	"LOWDOWN_HIGHLIGHT",            /* LOWDOWN_HIGHLIGHT */
	"LOWDOWN_IMAGE",                /* LOWDOWN_IMAGE */
	"LOWDOWN_LINEBREAK",            /* LOWDOWN_LINEBREAK */
	"LOWDOWN_LINK",                 /* LOWDOWN_LINK */
	"LOWDOWN_TRIPLE_EMPHASIS",      /* LOWDOWN_TRIPLE_EMPHASIS */
	"LOWDOWN_STRIKETHROUGH",        /* LOWDOWN_STRIKETHROUGH */
	"LOWDOWN_SUBSCRIPT",		/* LOWDOWN_SUBSCRIPT */
	"LOWDOWN_SUPERSCRIPT",          /* LOWDOWN_SUPERSCRIPT */
	"LOWDOWN_FOOTNOTE",		/* LOWDOWN_FOOTNOTE */
	"LOWDOWN_MATH_BLOCK",           /* LOWDOWN_MATH_BLOCK */
	"LOWDOWN_RAW_HTML",             /* LOWDOWN_RAW_HTML */
	"LOWDOWN_ENTITY",               /* LOWDOWN_ENTITY */
	"LOWDOWN_NORMAL_TEXT",          /* LOWDOWN_NORMAL_TEXT */
	"LOWDOWN_DOC_HEADER",           /* LOWDOWN_DOC_HEADER */
	"LOWDOWN_META",			/* LOWDOWN_META */
};

static int
rndr_indent(struct lowdown_buf *ob, size_t indent)
{
	size_t	 i;

	for (i = 0; i < indent; i++)
		if (!HBUF_PUTSL(ob, "  "))
			return 0;
	return 1;
}

static int
rndr_short(struct lowdown_buf *ob, const struct lowdown_buf *b)
{
	size_t	 	 i;
	unsigned char	 ch;

	for (i = 0; i < 20 && i < b->size; i++) {
		ch = (unsigned char)b->data[i];
		if (ch == '\n') {
			if (!HBUF_PUTSL(ob, "\\n"))
				return 0;
		} else if (ch == '\r') {
			if (!HBUF_PUTSL(ob, "\\r"))
				return 0;
		} else if (ch == '\t') {
			if (!HBUF_PUTSL(ob, "\\t"))
				return 0;
		} else if (ch < 0x80 && iscntrl(ch)) {
			if (!hbuf_putc(ob, '?'))
				return 0;
		} else {
			if (!hbuf_putc(ob, b->data[i]))
				return 0;
		}
	}

	if (i < b->size && !HBUF_PUTSL(ob, "..."))
		return 0;
	return 1;
}

static int
rndr(struct lowdown_buf *ob, struct lowdown_metaq *mq,
	const struct lowdown_node *root, size_t indent)
{
	const struct lowdown_node	*n;
	struct lowdown_buf		*tmp;

	if (!rndr_indent(ob, indent))
		return 0;
	if (root->chng == LOWDOWN_CHNG_INSERT && 
	    !HBUF_PUTSL(ob, "INSERT: "))
		return 0;
	if (root->chng == LOWDOWN_CHNG_DELETE && 
	    !HBUF_PUTSL(ob, "DELETE: "))
		return 0;
	if (!hbuf_printf(ob, "%s (%zu)", names[root->type], root->id))
		return 0;
	if (!hbuf_putc(ob, '\n'))
		return 0;

	switch (root->type) {
	case LOWDOWN_PARAGRAPH:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "lines: %zu, blank-after: %d\n", 
		    root->rndr_paragraph.lines,
		    root->rndr_paragraph.beoln))
			return 0;
		break;
	case LOWDOWN_IMAGE:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "source: "))
			return 0;
		if (!rndr_short(ob, &root->rndr_image.link))
			return 0;
		if (root->rndr_image.dims.size) {
			if (!HBUF_PUTSL(ob, "("))
				return 0;
			if (!rndr_short(ob, &root->rndr_image.dims))
				return 0;
			if (!HBUF_PUTSL(ob, ")"))
				return 0;
		}
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		if (root->rndr_image.title.size) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!hbuf_printf(ob, "title: "))
				return 0;
			if (!rndr_short(ob, &root->rndr_image.title))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_image.alt.size) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!hbuf_printf(ob, "alt: "))
				return 0;
			if (!rndr_short(ob, &root->rndr_image.alt))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_image.dims.size) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!hbuf_printf(ob, "dims: "))
				return 0;
			if (!rndr_short(ob, &root->rndr_image.dims))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_image.attr_width.size) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!hbuf_printf(ob, "width (extended): "))
				return 0;
			if (!rndr_short(ob, &root->rndr_image.attr_width))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_image.attr_height.size) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!hbuf_printf(ob, "height (extended): "))
				return 0;
			if (!rndr_short(ob, &root->rndr_image.attr_height))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_image.attr_cls.size > 0) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!HBUF_PUTSL(ob, "class: "))
				return 0;
			if (!hbuf_putb(ob, &root->rndr_image.attr_cls))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_image.attr_id.size > 0) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!HBUF_PUTSL(ob, "id: "))
				return 0;
			if (!hbuf_putb(ob, &root->rndr_image.attr_id))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		break;
	case LOWDOWN_HEADER:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "level: %zu\n",
		    root->rndr_header.level))
			return 0;
		if (root->rndr_header.attr_cls.size > 0) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!HBUF_PUTSL(ob, "class: "))
				return 0;
			if (!hbuf_putb(ob, &root->rndr_header.attr_cls))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_header.attr_id.size > 0) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!HBUF_PUTSL(ob, "id: "))
				return 0;
			if (!hbuf_putb(ob, &root->rndr_header.attr_id))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		break;
	case LOWDOWN_RAW_HTML:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "data: %zu Bytes: ",
		    root->rndr_raw_html.text.size))
			return 0;
		if (!rndr_short(ob, &root->rndr_raw_html.text))
			return 0;
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		break;
	case LOWDOWN_BLOCKHTML:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "data: %zu Bytes: ",
		    root->rndr_blockhtml.text.size))
			return 0;
		if (!rndr_short(ob, &root->rndr_blockhtml.text))
			return 0;
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		break;
	case LOWDOWN_BLOCKQUOTE:
		if (root->rndr_blockquote.type == BLOCKQUOTE_REGULAR)
			break;
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "admonition (%s): %s\n",
		    root->rndr_blockquote.type == BLOCKQUOTE_ADMONITION ?
		    "single-line" : "double-line",
		    root->rndr_blockquote.admonition == ADMONITION_NOTE ?
		    "note" :
		    root->rndr_blockquote.admonition == ADMONITION_WARNING ?
		    "warning" : "callout"))
			return 0;
		break;
	case LOWDOWN_BLOCKCODE:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "data: %zu Bytes: ",
		    root->rndr_blockcode.text.size))
			return 0;
		if (!rndr_short(ob, &root->rndr_blockcode.text))
			return 0;
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		break;
	case LOWDOWN_DEFINITION:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "scope: %s\n",
		    HLIST_FL_BLOCK & root->rndr_definition.flags ? 
		    "block" : "span"))
			return 0;
		break;
	case LOWDOWN_TABLE_BLOCK:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "columns: %zu\n", 
		    root->rndr_table.columns))
			return 0;
		break;
	case LOWDOWN_TABLE_CELL:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "current: %zu\n", 
		    root->rndr_table_cell.col))
			return 0;
		break;
	case LOWDOWN_LISTITEM:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "scope: %s\n",
		    (root->rndr_listitem.flags & HLIST_FL_BLOCK) ?
		    "block" : "span"))
			return 0;
		if (!(root->rndr_listitem.flags &
		     (HLIST_FL_CHECKED | HLIST_FL_UNCHECKED)))
			break;
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "check status: %s\n",
		    (root->rndr_listitem.flags & HLIST_FL_CHECKED) ?
		    "checked" : "unchecked"))
			return 0;
		break;
	case LOWDOWN_LIST:
		if (!rndr_indent(ob, indent + 1) ||
		    !hbuf_printf(ob, "list type: %s\n",
		     (root->rndr_list.flags & HLIST_FL_ORDERED) ?
		      "ordered" : "unordered") ||
		    !rndr_indent(ob, indent + 1) ||
		    !hbuf_printf(ob, "scope: %s\n",
		     (root->rndr_list.flags & HLIST_FL_BLOCK) ?
		      "block" : "span") ||
		    !rndr_indent(ob, indent + 1) ||
		    !hbuf_printf(ob, "list items: %zu\n",
		     root->rndr_list.items))
			return 0;
		break;
	case LOWDOWN_META:
		if (lowdown_get_meta(root, mq) == NULL)
			return 0;
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "key: "))
			return 0;
		if (!rndr_short(ob, &root->rndr_meta.key))
			return 0;
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		break;
	case LOWDOWN_MATH_BLOCK:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "blockmode: %s\n",
		    root->rndr_math.blockmode ?
		    "block" : "inline"))
			return 0;
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "data: %zu Bytes: ",
		    root->rndr_math.text.size))
			return 0;
		if (!rndr_short(ob, &root->rndr_math.text))
			return 0;
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		break;
	case LOWDOWN_ENTITY:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "value: "))
			return 0;
		if (!rndr_short(ob, &root->rndr_entity.text))
			return 0;
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		break;
	case LOWDOWN_LINK_AUTO:
		if (root->rndr_autolink.link.size) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!HBUF_PUTSL(ob, "link: "))
				return 0;
			if (!rndr_short(ob, &root->rndr_autolink.link))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		break;
	case LOWDOWN_LINK:
		if (root->rndr_link.title.size) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!HBUF_PUTSL(ob, "title: "))
				return 0;
			if (!rndr_short(ob, &root->rndr_link.title))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_link.link.size) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!HBUF_PUTSL(ob, "link: "))
				return 0;
			if (!rndr_short(ob, &root->rndr_link.link))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_link.attr_cls.size > 0) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!HBUF_PUTSL(ob, "class: "))
				return 0;
			if (!hbuf_putb(ob, &root->rndr_link.attr_cls))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		if (root->rndr_link.attr_id.size > 0) {
			if (!rndr_indent(ob, indent + 1))
				return 0;
			if (!HBUF_PUTSL(ob, "id: "))
				return 0;
			if (!hbuf_putb(ob, &root->rndr_link.attr_id))
				return 0;
			if (!HBUF_PUTSL(ob, "\n"))
				return 0;
		}
		break;
	case LOWDOWN_NORMAL_TEXT:
		if (!rndr_indent(ob, indent + 1))
			return 0;
		if (!hbuf_printf(ob, "data: %zu Bytes: ",
		    root->rndr_normal_text.text.size))
			return 0;
		if (!rndr_short(ob, &root->rndr_normal_text.text))
			return 0;
		if (!HBUF_PUTSL(ob, "\n"))
			return 0;
		break;
	default:
		break;
	}

	if ((tmp = hbuf_new(64)) == NULL)
		return 0;

	TAILQ_FOREACH(n, &root->children, entries)
		if (!rndr(tmp, mq, n, indent + 1)) {
			hbuf_free(tmp);
			return 0;
		}

	hbuf_putb(ob, tmp);
	hbuf_free(tmp);
	return 1;
}

int
lowdown_tree_rndr(struct lowdown_buf *ob,
	const struct lowdown_node *root,
	const struct lowdown_opts *opts)
{
	struct lowdown_buf	*obtmp = NULL, *mqtmp = NULL;
	struct lowdown_metaq	 mq;
	struct lowdown_meta	*m;
	int			 rc = 0;
	size_t			 init_depth = 0;

	TAILQ_INIT(&mq);

	if ((obtmp = hbuf_new(64)) == NULL)
		goto out;
	if ((mqtmp = hbuf_new(64)) == NULL)
		goto out;

	/*
	 * Output the parsed document into obtmp, with optional envelope
	 * in standalone mode.
	 */

	if (opts != NULL && (opts->oflags & LOWDOWN_STANDALONE)) {
		init_depth = 1;
		if (!HBUF_PUTSL(obtmp, "document:\n"))
			goto out;
	}
	if (!rndr(obtmp, &mq, root, init_depth))
		goto out;

	/* Optionally output the parsed metadata into mqtmp. */

	if (opts != NULL && (opts->oflags & LOWDOWN_STANDALONE)) {
		if (!HBUF_PUTSL(mqtmp, "metadata:\n"))
			return 0;
		TAILQ_FOREACH(m, &mq, entries)
			if (!hbuf_printf(mqtmp, "  %s: %s\n", m->key,
			    m->value))
				return 0;
	}

	/*
	 * If a template has been provided in standalone mode, print
	 * that after the body and metadata.  Otherwise, directly print
	 * the body (and optional metadata).
	 */

	if (opts != NULL && opts->templ != NULL &&
	    (opts->oflags & LOWDOWN_STANDALONE)) {
		if (!(hbuf_putb(ob, obtmp) && hbuf_putb(ob, mqtmp)))
			return 0;
		if (!HBUF_PUTSL(ob, "template:\n"))
			return 0;
		rc = lowdown_template(opts->templ, obtmp, ob, &mq, 1);
	} else
		rc = hbuf_putb(ob, obtmp) && hbuf_putb(ob, mqtmp);

out:
	lowdown_metaq_free(&mq);
	hbuf_free(obtmp);
	hbuf_free(mqtmp);
	return rc;
}

