/*
 * Copyright (c) 2015, Vicent Marti
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "redcarpet.h"

#define SPAN_CALLBACK(method_name, ...) {\
	struct redcarpet_renderopt *opt = opaque;\
	VALUE ret = rb_funcall(opt->self, rb_intern(method_name), __VA_ARGS__);\
	if (NIL_P(ret)) return 0;\
	Check_Type(ret, T_STRING);\
	bufput(ob, RSTRING_PTR(ret), RSTRING_LEN(ret));\
	return 1;\
}

#define BLOCK_CALLBACK(method_name, ...) {\
	struct redcarpet_renderopt *opt = opaque;\
	VALUE ret = rb_funcall(opt->self, rb_intern(method_name), __VA_ARGS__);\
	if (NIL_P(ret)) return;\
	Check_Type(ret, T_STRING);\
	bufput(ob, RSTRING_PTR(ret), RSTRING_LEN(ret));\
}

extern VALUE rb_mRedcarpet;
extern VALUE rb_cRenderHTML_TOC;
VALUE rb_mRender;
VALUE rb_cRenderBase;
VALUE rb_cRenderHTML;
VALUE rb_mSmartyPants;

#define buf2str(t) ((t) ? rb_enc_str_new((const char*)(t)->data, (t)->size, opt->active_enc) : Qnil)

static void
rndr_blockcode(struct buf *ob, const struct buf *text, const struct buf *lang, void *opaque)
{
	BLOCK_CALLBACK("block_code", 2, buf2str(text), buf2str(lang));
}

static void
rndr_blockquote(struct buf *ob, const struct buf *text, void *opaque)
{
	BLOCK_CALLBACK("block_quote", 1, buf2str(text));
}

static void
rndr_raw_block(struct buf *ob, const struct buf *text, void *opaque)
{
	BLOCK_CALLBACK("block_html", 1, buf2str(text));
}

static void
rndr_header(struct buf *ob, const struct buf *text, int level, void *opaque)
{
	BLOCK_CALLBACK("header", 2, buf2str(text), INT2FIX(level));
}

static void
rndr_hrule(struct buf *ob, void *opaque)
{
	BLOCK_CALLBACK("hrule", 0);
}

static void
rndr_list(struct buf *ob, const struct buf *text, int flags, void *opaque)
{
	BLOCK_CALLBACK("list", 2, buf2str(text),
			(flags & MKD_LIST_ORDERED) ? CSTR2SYM("ordered") : CSTR2SYM("unordered"));
}

static void
rndr_listitem(struct buf *ob, const struct buf *text, int flags, void *opaque)
{
	BLOCK_CALLBACK("list_item", 2, buf2str(text),
			(flags & MKD_LIST_ORDERED) ? CSTR2SYM("ordered") : CSTR2SYM("unordered"));
}

static void
rndr_paragraph(struct buf *ob, const struct buf *text, void *opaque)
{
	BLOCK_CALLBACK("paragraph", 1, buf2str(text));
}

static void
rndr_table(struct buf *ob, const struct buf *header, const struct buf *body, void *opaque)
{
	BLOCK_CALLBACK("table", 2, buf2str(header), buf2str(body));
}

static void
rndr_tablerow(struct buf *ob, const struct buf *text, void *opaque)
{
	BLOCK_CALLBACK("table_row", 1, buf2str(text));
}

static void
rndr_tablecell(struct buf *ob, const struct buf *text, int align, void *opaque)
{
	VALUE rb_align;

	switch (align) {
	case MKD_TABLE_ALIGN_L:
		rb_align = CSTR2SYM("left");
		break;

	case MKD_TABLE_ALIGN_R:
		rb_align = CSTR2SYM("right");
		break;

	case MKD_TABLE_ALIGN_CENTER:
		rb_align = CSTR2SYM("center");
		break;

	default:
		rb_align = Qnil;
		break;
	}

	BLOCK_CALLBACK("table_cell", 2, buf2str(text), rb_align);
}

static void
rndr_footnotes(struct buf *ob, const struct buf *text, void *opaque)
{
	BLOCK_CALLBACK("footnotes", 1, buf2str(text));
}

static void
rndr_footnote_def(struct buf *ob, const struct buf *text, unsigned int num, void *opaque)
{
	BLOCK_CALLBACK("footnote_def", 2, buf2str(text), INT2FIX(num));
}


/***
 * SPAN LEVEL
 */
static int
rndr_autolink(struct buf *ob, const struct buf *link, enum mkd_autolink type, void *opaque)
{
	SPAN_CALLBACK("autolink", 2, buf2str(link),
		type == MKDA_NORMAL ? CSTR2SYM("url") : CSTR2SYM("email"));
}

static int
rndr_codespan(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("codespan", 1, buf2str(text));
}

static int
rndr_double_emphasis(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("double_emphasis", 1, buf2str(text));
}

static int
rndr_emphasis(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("emphasis", 1, buf2str(text));
}

static int
rndr_underline(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("underline", 1, buf2str(text));
}

static int
rndr_highlight(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("highlight", 1, buf2str(text));
}

static int
rndr_quote(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("quote", 1, buf2str(text));
}

static int
rndr_image(struct buf *ob, const struct buf *link, const struct buf *title, const struct buf *alt, void *opaque)
{
	SPAN_CALLBACK("image", 3, buf2str(link), buf2str(title), buf2str(alt));
}

static int
rndr_linebreak(struct buf *ob, void *opaque)
{
	SPAN_CALLBACK("linebreak", 0);
}

static int
rndr_link(struct buf *ob, const struct buf *link, const struct buf *title, const struct buf *content, void *opaque)
{
	SPAN_CALLBACK("link", 3, buf2str(link), buf2str(title), buf2str(content));
}

static int
rndr_raw_html(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("raw_html", 1, buf2str(text));
}

static int
rndr_triple_emphasis(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("triple_emphasis", 1, buf2str(text));
}

static int
rndr_strikethrough(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("strikethrough", 1, buf2str(text));
}

static int
rndr_superscript(struct buf *ob, const struct buf *text, void *opaque)
{
	SPAN_CALLBACK("superscript", 1, buf2str(text));
}

static int
rndr_footnote_ref(struct buf *ob, unsigned int num, void *opaque)
{
	SPAN_CALLBACK("footnote_ref", 1, INT2FIX(num));
}

/**
 * direct writes
 */
static void
rndr_entity(struct buf *ob, const struct buf *text, void *opaque)
{
	BLOCK_CALLBACK("entity", 1, buf2str(text));
}

static void
rndr_normal_text(struct buf *ob, const struct buf *text, void *opaque)
{
	BLOCK_CALLBACK("normal_text", 1, buf2str(text));
}

static void
rndr_doc_header(struct buf *ob, void *opaque)
{
	BLOCK_CALLBACK("doc_header", 0);
}

static void
rndr_doc_footer(struct buf *ob, void *opaque)
{
	BLOCK_CALLBACK("doc_footer", 0);
}

static int
cb_link_attribute(VALUE key, VALUE val, VALUE payload)
{
	struct buf *ob = (struct buf *)payload;
	key = rb_obj_as_string(key);
	val = rb_obj_as_string(val);
	bufprintf(ob, " %s=\"%s\"", StringValueCStr(key), StringValueCStr(val));
	return 0;
}

static void
rndr_link_attributes(struct buf *ob, const struct buf *url, void *opaque)
{
	struct redcarpet_renderopt *opt = opaque;
	struct rb_redcarpet_rndr *rndr;

	Data_Get_Struct(opt->self, struct rb_redcarpet_rndr, rndr);
	Check_Type(opt->link_attributes, T_HASH);
	rb_hash_foreach(opt->link_attributes, &cb_link_attribute, (VALUE)ob);
}

static struct sd_callbacks rb_redcarpet_callbacks = {
	rndr_blockcode,
	rndr_blockquote,
	rndr_raw_block,
	rndr_header,
	rndr_hrule,
	rndr_list,
	rndr_listitem,
	rndr_paragraph,
	rndr_table,
	rndr_tablerow,
	rndr_tablecell,
	rndr_footnotes,
	rndr_footnote_def,

	rndr_autolink,
	rndr_codespan,
	rndr_double_emphasis,
	rndr_emphasis,
	rndr_underline,
	rndr_highlight,
	rndr_quote,
	rndr_image,
	rndr_linebreak,
	rndr_link,
	rndr_raw_html,
	rndr_triple_emphasis,
	rndr_strikethrough,
	rndr_superscript,
	rndr_footnote_ref,

	rndr_entity,
	rndr_normal_text,

	rndr_doc_header,
	rndr_doc_footer,
};

static const char *rb_redcarpet_method_names[] = {
	"block_code",
	"block_quote",
	"block_html",
	"header",
	"hrule",
	"list",
	"list_item",
	"paragraph",
	"table",
	"table_row",
	"table_cell",
	"footnotes",
	"footnote_def",

	"autolink",
	"codespan",
	"double_emphasis",
	"emphasis",
	"underline",
	"highlight",
	"quote",
	"image",
	"linebreak",
	"link",
	"raw_html",
	"triple_emphasis",
	"strikethrough",
	"superscript",
	"footnote_ref",

	"entity",
	"normal_text",

	"doc_header",
	"doc_footer"
};

static const size_t rb_redcarpet_method_count = sizeof(rb_redcarpet_method_names)/sizeof(char *);

static void rb_redcarpet_rbase_mark(struct rb_redcarpet_rndr *rndr)
{
	if (rndr->options.link_attributes)
		rb_gc_mark(rndr->options.link_attributes);
}

static void rndr_deallocate(void *rndr)
{
  xfree(rndr);
}

static VALUE rb_redcarpet_rbase_alloc(VALUE klass)
{
	struct rb_redcarpet_rndr *rndr = ALLOC(struct rb_redcarpet_rndr);
	memset(rndr, 0x0, sizeof(struct rb_redcarpet_rndr));
	return Data_Wrap_Struct(klass, rb_redcarpet_rbase_mark, rndr_deallocate, rndr);
}

static void rb_redcarpet__overload(VALUE self, VALUE base_class)
{
	struct rb_redcarpet_rndr *rndr;
	VALUE options_ivar;

	Data_Get_Struct(self, struct rb_redcarpet_rndr, rndr);
	rndr->options.self = self;
	rndr->options.base_class = base_class;

	if (rb_obj_class(self) == rb_cRenderBase)
		rb_raise(rb_eRuntimeError,
			"The Redcarpet::Render::Base class cannot be instantiated. "
			"Create an inheriting class instead to implement a custom renderer.");

	if (rb_obj_class(self) != base_class) {
		void **source = (void **)&rb_redcarpet_callbacks;
		void **dest = (void **)&rndr->callbacks;
		size_t i;

		for (i = 0; i < rb_redcarpet_method_count; ++i) {
			if (rb_respond_to(self, rb_intern(rb_redcarpet_method_names[i])))
				dest[i] = source[i];
		}
	}

	options_ivar = rb_attr_get(self, rb_intern("@options"));
	if (options_ivar == Qundef || options_ivar == Qnil)
		rb_iv_set(self, "@options", rb_hash_new());
}

static VALUE rb_redcarpet_rbase_init(VALUE self)
{
	rb_redcarpet__overload(self, rb_cRenderBase);
	return Qnil;
}

static VALUE rb_redcarpet_html_init(int argc, VALUE *argv, VALUE self)
{
	struct rb_redcarpet_rndr *rndr;
	unsigned int render_flags = 0;
	VALUE hash, link_attr = Qnil;

	Data_Get_Struct(self, struct rb_redcarpet_rndr, rndr);

	if (rb_scan_args(argc, argv, "01", &hash) == 1) {
		Check_Type(hash, T_HASH);

		/* Give access to the passed options through `@options` */
		rb_iv_set(self, "@options", hash);

		/* escape_html */
		if (rb_hash_aref(hash, CSTR2SYM("escape_html")) == Qtrue)
			render_flags |= HTML_ESCAPE;

		/* filter_html */
		if (rb_hash_aref(hash, CSTR2SYM("filter_html")) == Qtrue)
			render_flags |= HTML_SKIP_HTML;

		/* no_image */
		if (rb_hash_aref(hash, CSTR2SYM("no_images")) == Qtrue)
			render_flags |= HTML_SKIP_IMAGES;

		/* no_links */
		if (rb_hash_aref(hash, CSTR2SYM("no_links")) == Qtrue)
			render_flags |= HTML_SKIP_LINKS;

		/* prettify */
		if (rb_hash_aref(hash, CSTR2SYM("prettify")) == Qtrue)
			render_flags |= HTML_PRETTIFY;

		/* filter_style */
		if (rb_hash_aref(hash, CSTR2SYM("no_styles")) == Qtrue)
			render_flags |= HTML_SKIP_STYLE;

		/* safelink */
		if (rb_hash_aref(hash, CSTR2SYM("safe_links_only")) == Qtrue)
			render_flags |= HTML_SAFELINK;

		if (rb_hash_aref(hash, CSTR2SYM("with_toc_data")) == Qtrue)
			render_flags |= HTML_TOC;

		if (rb_hash_aref(hash, CSTR2SYM("hard_wrap")) == Qtrue)
			render_flags |= HTML_HARD_WRAP;

		if (rb_hash_aref(hash, CSTR2SYM("xhtml")) == Qtrue)
			render_flags |= HTML_USE_XHTML;

		link_attr = rb_hash_aref(hash, CSTR2SYM("link_attributes"));
	}

	sdhtml_renderer(&rndr->callbacks, (struct html_renderopt *)&rndr->options.html, render_flags);
	rb_redcarpet__overload(self, rb_cRenderHTML);

	if (!NIL_P(link_attr)) {
		rndr->options.link_attributes = link_attr;
		rndr->options.html.link_attributes = &rndr_link_attributes;
	}

	return Qnil;
}

static VALUE rb_redcarpet_htmltoc_init(int argc, VALUE *argv, VALUE self)
{
	struct rb_redcarpet_rndr *rndr;
	unsigned int render_flags = HTML_TOC;
	VALUE hash, nesting_level = Qnil;

	Data_Get_Struct(self, struct rb_redcarpet_rndr, rndr);

	if (rb_scan_args(argc, argv, "01", &hash) == 1) {
		Check_Type(hash, T_HASH);

		/* Give access to the passed options through `@options` */
		rb_iv_set(self, "@options", hash);

		/* escape_html */
		if (rb_hash_aref(hash, CSTR2SYM("escape_html")) == Qtrue)
			render_flags |= HTML_ESCAPE;

		/* Nesting level */
		nesting_level = rb_hash_aref(hash, CSTR2SYM("nesting_level"));
	}

	sdhtml_toc_renderer(&rndr->callbacks, (struct html_renderopt *)&rndr->options.html, render_flags);
	rb_redcarpet__overload(self, rb_cRenderHTML_TOC);

	/* Check whether we are dealing with a Range object by
	   checking whether the object responds to min and max */
	if (rb_respond_to(nesting_level, rb_intern("min")) &&
	    rb_respond_to(nesting_level, rb_intern("max"))) {
		int min = NUM2INT(rb_funcall(nesting_level, rb_intern("min"), 0));
		int max = NUM2INT(rb_funcall(nesting_level, rb_intern("max"), 0));

		rndr->options.html.toc_data.nesting_bounds[0] = min;
		rndr->options.html.toc_data.nesting_bounds[1] = max;
	} else if (FIXNUM_P(nesting_level)) {
		rndr->options.html.toc_data.nesting_bounds[0] = 1;
		rndr->options.html.toc_data.nesting_bounds[1] = NUM2INT(nesting_level);
	} else {
		rndr->options.html.toc_data.nesting_bounds[0] = 1;
		rndr->options.html.toc_data.nesting_bounds[1] = 6;
	}

	return Qnil;
}

static VALUE rb_redcarpet_smartypants_render(VALUE self, VALUE text)
{
	VALUE result;
	struct buf *output_buf;

	Check_Type(text, T_STRING);

	output_buf = bufnew(128);

	sdhtml_smartypants(output_buf, (const uint8_t*)RSTRING_PTR(text), RSTRING_LEN(text));
	result = rb_enc_str_new((const char*)output_buf->data, output_buf->size, rb_enc_get(text));

	bufrelease(output_buf);
	return result;
}

void Init_redcarpet_rndr()
{
	rb_mRender = rb_define_module_under(rb_mRedcarpet, "Render");

	rb_cRenderBase = rb_define_class_under(rb_mRender, "Base", rb_cObject);
	rb_define_alloc_func(rb_cRenderBase, rb_redcarpet_rbase_alloc);
	rb_define_method(rb_cRenderBase, "initialize", rb_redcarpet_rbase_init, 0);

	rb_cRenderHTML = rb_define_class_under(rb_mRender, "HTML", rb_cRenderBase);
	rb_define_method(rb_cRenderHTML, "initialize", rb_redcarpet_html_init, -1);

	rb_cRenderHTML_TOC = rb_define_class_under(rb_mRender, "HTML_TOC", rb_cRenderBase);
	rb_define_method(rb_cRenderHTML_TOC, "initialize", rb_redcarpet_htmltoc_init, -1);

	rb_mSmartyPants = rb_define_module_under(rb_mRender, "SmartyPants");
	rb_define_method(rb_mSmartyPants, "postprocess", rb_redcarpet_smartypants_render, 1);
}
