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

#include "config.h"
#include "cmark.h"
#include "node.h"
#include "buffer.h"
#include "utf8.h"
#include "scanners.h"

// Functions to convert cmark_nodes to commonmark strings.

struct render_state {
	int options;
	cmark_strbuf* buffer;
	cmark_strbuf* prefix;
	int column;
	int width;
	int need_cr;
	int last_breakable;
	bool begin_line;
	bool no_wrap;
	bool in_tight_list_item;
};

static inline void cr(struct render_state *state)
{
	if (state->need_cr < 1) {
		state->need_cr = 1;
	}
}

static inline void blankline(struct render_state *state)
{
	if (state->need_cr < 2) {
		state->need_cr = 2;
	}
}

typedef enum  {
	LITERAL,
	NORMAL,
	TITLE,
	URL
} escaping;

static inline bool
needs_escaping(escaping escape,
               int32_t c,
               unsigned char next_c,
               struct render_state *state)
{
	if (escape == NORMAL) {
		return (c == '*' || c == '_' || c == '[' || c == ']' ||
		        c == '<' || c == '>' || c == '\\' || c == '`' ||
		        (c == '&' && isalpha(next_c)) ||
		        (c == '!' && next_c == '[') ||
		        (state->begin_line &&
		         (c == '-' || c == '+' || c == '#' || c == '=')) ||
		        (c == '#' && (isspace(next_c) || next_c == '\0')) ||
		        ((c == '.' || c == ')') &&
		         isdigit(state->buffer->ptr[state->buffer->size - 1])));
	} else if (escape == TITLE) {
		return (c == '`' || c == '<' || c == '>' || c == '"' ||
		        c == '\\');
	} else if (escape == URL) {
		return (c == '`' || c == '<' || c == '>' || isspace(c) ||
		        c == '\\' || c == ')' || c == '(');
	} else {
		return false;
	}
}

static inline void out(struct render_state *state,
                       cmark_chunk str,
                       bool wrap,
                       escaping escape)
{
	unsigned char* source = str.data;
	int length = str.len;
	unsigned char nextc;
	int32_t c;
	int i = 0;
	int len;
	cmark_chunk remainder = cmark_chunk_literal("");
	int k = state->buffer->size - 1;

	wrap = wrap && !state->no_wrap;

	if (state->in_tight_list_item && state->need_cr > 1) {
		state->need_cr = 1;
	}
	while (state->need_cr) {
		if (k < 0 || state->buffer->ptr[k] == '\n') {
			k -= 1;
		} else {
			cmark_strbuf_putc(state->buffer, '\n');
			if (state->need_cr > 1) {
				cmark_strbuf_put(state->buffer, state->prefix->ptr,
				                 state->prefix->size);
			}
		}
		state->column = 0;
		state->begin_line = true;
		state->need_cr -= 1;
	}

	while (i < length) {
		if (state->begin_line) {
			cmark_strbuf_put(state->buffer, state->prefix->ptr,
			                 state->prefix->size);
			// note: this assumes prefix is ascii:
			state->column = state->prefix->size;
		}

		len = utf8proc_iterate(source + i, length - i, &c);
		if (len == -1) { // error condition
			return;  // return without rendering rest of string
		}
		nextc = source[i + len];
		if (c == 32 && wrap) {
			if (!state->begin_line) {
				cmark_strbuf_putc(state->buffer, ' ');
				state->column += 1;
				state->begin_line = false;
				state->last_breakable = state->buffer->size -
				                        1;
				// skip following spaces
				while (source[i + 1] == ' ') {
					i++;
				}
			}

		} else if (c == 10) {
			cmark_strbuf_putc(state->buffer, '\n');
			state->column = 0;
			state->begin_line = true;
			state->last_breakable = 0;
		} else if (needs_escaping(escape, c, nextc, state)) {
			if (isspace(c)) {
				// use percent encoding for spaces
				cmark_strbuf_printf(state->buffer, "%%%2x", c);
				state->column += 3;
			} else {
				cmark_strbuf_putc(state->buffer, '\\');
				utf8proc_encode_char(c, state->buffer);
				state->column += 2;
			}
			state->begin_line = false;
		} else {
			utf8proc_encode_char(c, state->buffer);
			state->column += 1;
			state->begin_line = false;
		}

		// If adding the character went beyond width, look for an
		// earlier place where the line could be broken:
		if (state->width > 0 &&
		    state->column > state->width &&
		    !state->begin_line &&
		    state->last_breakable > 0) {

			// copy from last_breakable to remainder
			cmark_chunk_set_cstr(&remainder, (char *) state->buffer->ptr + state->last_breakable + 1);
			// truncate at last_breakable
			cmark_strbuf_truncate(state->buffer, state->last_breakable);
			// add newline, prefix, and remainder
			cmark_strbuf_putc(state->buffer, '\n');
			cmark_strbuf_put(state->buffer, state->prefix->ptr,
			                 state->prefix->size);
			cmark_strbuf_put(state->buffer, remainder.data, remainder.len);
			state->column = state->prefix->size + remainder.len;
			cmark_chunk_free(&remainder);
			state->last_breakable = 0;
			state->begin_line = false;
		}

		i += len;
	}
}

static void lit(struct render_state *state, char *s, bool wrap)
{
	cmark_chunk str = cmark_chunk_literal(s);
	out(state, str, wrap, LITERAL);
}

static int
longest_backtick_sequence(cmark_chunk *code)
{
	int longest = 0;
	int current = 0;
	int i = 0;
	while (i <= code->len) {
		if (code->data[i] == '`') {
			current++;
		} else {
			if (current > longest) {
				longest = current;
			}
			current = 0;
		}
		i++;
	}
	return longest;
}

static int
shortest_unused_backtick_sequence(cmark_chunk *code)
{
	int32_t used = 1;
	int current = 0;
	int i = 0;
	while (i <= code->len) {
		if (code->data[i] == '`') {
			current++;
		} else {
			if (current) {
				used |= (1 << current);
			}
			current = 0;
		}
		i++;
	}
	// return number of first bit that is 0:
	i = 0;
	while (used & 1) {
		used = used >> 1;
		i++;
	}
	return i;
}

static bool
is_autolink(cmark_node *node)
{
	const char *title;
	const char *url;

	if (node->type != CMARK_NODE_LINK) {
		return false;
	}

	url = cmark_node_get_url(node);
	if (url == NULL ||
	    _scan_scheme((unsigned char *)url) == 0) {
		return false;
	}

	title = cmark_node_get_title(node);
	// if it has a title, we can't treat it as an autolink:
	if (title != NULL && strlen(title) > 0) {
		return false;
	}
	cmark_consolidate_text_nodes(node);
	return (strncmp(url,
	                (char*)node->as.literal.data,
	                node->as.literal.len) == 0);
}

// if node is a block node, returns node.
// otherwise returns first block-level node that is an ancestor of node.
static cmark_node*
get_containing_block(cmark_node *node)
{
	while (node &&
	       (node->type < CMARK_NODE_FIRST_BLOCK ||
	        node->type > CMARK_NODE_LAST_BLOCK)) {
		node = node->parent;
	}
	return node;
}

static int
S_render_node(cmark_node *node, cmark_event_type ev_type,
              struct render_state *state)
{
	cmark_node *tmp;
	cmark_chunk *code;
	int list_number;
	cmark_delim_type list_delim;
	int numticks;
	int i;
	bool entering = (ev_type == CMARK_EVENT_ENTER);
	const char *info;
	const char *title;
	cmark_strbuf listmarker = GH_BUF_INIT;
	char *emph_delim;
	int marker_width;

	// Don't adjust tight list status til we've started the list.
	// Otherwise we loose the blank line between a paragraph and
	// a following list.
	if (!(node->type == CMARK_NODE_ITEM && node->prev == NULL &&
	      entering)) {
		tmp = get_containing_block(node);
		state->in_tight_list_item =
		    (tmp->type == CMARK_NODE_ITEM &&
		     cmark_node_get_list_tight(tmp->parent)) ||
		    (tmp &&
		     tmp->parent &&
		     tmp->parent->type == CMARK_NODE_ITEM &&
		     cmark_node_get_list_tight(tmp->parent->parent));
	}

	switch (node->type) {
	case CMARK_NODE_DOCUMENT:
		if (!entering) {
			cmark_strbuf_putc(state->buffer, '\n');
		}
		break;

	case CMARK_NODE_BLOCK_QUOTE:
		if (entering) {
			lit(state, "> ", false);
			cmark_strbuf_puts(state->prefix, "> ");
		} else {
			cmark_strbuf_truncate(state->prefix,
			                      state->prefix->size - 2);
			blankline(state);
		}
		break;

	case CMARK_NODE_LIST:
		if (!entering && node->next &&
		    (node->next->type == CMARK_NODE_CODE_BLOCK ||
		     node->next->type == CMARK_NODE_LIST)) {
			// this ensures 2 blank lines after list,
			// if before code block or list:
			lit(state, "\n", false);
			state->need_cr = 0;
		}
		break;

	case CMARK_NODE_ITEM:
		if (cmark_node_get_list_type(node->parent) ==
		    CMARK_BULLET_LIST) {
			marker_width = 2;
		} else {
			list_number = cmark_node_get_list_start(node->parent);
			list_delim = cmark_node_get_list_delim(node->parent);
			tmp = node;
			while (tmp->prev) {
				tmp = tmp->prev;
				list_number += 1;
			}
			// we ensure a width of at least 4 so
			// we get nice transition from single digits
			// to double
			cmark_strbuf_printf(&listmarker,
			                    "%d%s%s", list_number,
			                    list_delim == CMARK_PAREN_DELIM ?
			                    ")" : ".",
			                    list_number < 10 ? "  " : " ");
			marker_width = listmarker.size;
		}
		if (entering) {
			if (cmark_node_get_list_type(node->parent) ==
			    CMARK_BULLET_LIST) {
				lit(state, "* ", false);
				cmark_strbuf_puts(state->prefix, "  ");
			} else {
				lit(state, (char *)listmarker.ptr, false);
				for (i = marker_width; i--;) {
					cmark_strbuf_putc(state->prefix, ' ');
				}
			}
		} else {
			cmark_strbuf_truncate(state->prefix,
			                      state->prefix->size -
			                      marker_width);
			cr(state);
		}
		cmark_strbuf_free(&listmarker);
		break;

	case CMARK_NODE_HEADER:
		if (entering) {
			for (int i = cmark_node_get_header_level(node); i > 0; i--) {
				lit(state, "#", false);
			}
			lit(state, " ", false);
			state->no_wrap = true;
		} else {
			state->no_wrap = false;
			blankline(state);
		}
		break;

	case CMARK_NODE_CODE_BLOCK:
		blankline(state);
		info = cmark_node_get_fence_info(node);
		code = &node->as.code.literal;
		// use indented form if no info, and code doesn't
		// begin or end with a blank line, and code isn't
		// first thing in a list item
		if ((info == NULL || strlen(info) == 0) &&
		    (code->len > 2 &&
		     !isspace(code->data[0]) &&
		     !(isspace(code->data[code->len - 1]) &&
		       isspace(code->data[code->len - 2]))) &&
		    !(node->prev == NULL && node->parent &&
		      node->parent->type == CMARK_NODE_ITEM)) {
			lit(state, "    ", false);
			cmark_strbuf_puts(state->prefix, "    ");
			out(state, node->as.code.literal, false, LITERAL);
			cmark_strbuf_truncate(state->prefix,
			                      state->prefix->size - 4);
		} else {
			numticks = longest_backtick_sequence(code) + 1;
			if (numticks < 3) {
				numticks = 3;
			}
			for (i = 0; i < numticks; i++) {
				lit(state, "`", false);
			}
			lit(state, " ", false);
			out(state, cmark_chunk_literal(info), false, LITERAL);
			cr(state);
			out(state, node->as.code.literal, false, LITERAL);
			cr(state);
			for (i = 0; i < numticks; i++) {
				lit(state, "`", false);
			}
		}
		blankline(state);
		break;

	case CMARK_NODE_HTML:
		blankline(state);
		out(state, node->as.literal, false, LITERAL);
		blankline(state);
		break;

	case CMARK_NODE_HRULE:
		blankline(state);
		lit(state, "-----", false);
		blankline(state);
		break;

	case CMARK_NODE_PARAGRAPH:
		if (!entering) {
			blankline(state);
		}
		break;

	case CMARK_NODE_TEXT:
		out(state, node->as.literal, true, NORMAL);
		break;

	case CMARK_NODE_LINEBREAK:
		if (!(CMARK_OPT_HARDBREAKS & state->options)) {
			lit(state, "\\", false);
		}
		cr(state);
		break;

	case CMARK_NODE_SOFTBREAK:
		if (state->width == 0) {
			cr(state);
		} else {
			lit(state, " ", true);
		}
		break;

	case CMARK_NODE_CODE:
		code = &node->as.literal;
		numticks = shortest_unused_backtick_sequence(code);
		for (i = 0; i < numticks; i++) {
			lit(state, "`", false);
		}
		if (code->len == 0 || code->data[0] == '`') {
			lit(state, " ", false);
		}
		out(state, node->as.literal, true, LITERAL);
		if (code->len == 0 || code->data[code->len - 1] == '`') {
			lit(state, " ", false);
		}
		for (i = 0; i < numticks; i++) {
			lit(state, "`", false);
		}
		break;

	case CMARK_NODE_INLINE_HTML:
		out(state, node->as.literal, false, LITERAL);
		break;

	case CMARK_NODE_STRONG:
		if (entering) {
			lit(state, "**", false);
		} else {
			lit(state, "**", false);
		}
		break;

	case CMARK_NODE_EMPH:
		// If we have EMPH(EMPH(x)), we need to use *_x_*
		// because **x** is STRONG(x):
		if (node->parent && node->parent->type == CMARK_NODE_EMPH &&
		    node->next == NULL && node->prev == NULL) {
			emph_delim = "_";
		} else {
			emph_delim = "*";
		}
		if (entering) {
			lit(state, emph_delim, false);
		} else {
			lit(state, emph_delim, false);
		}
		break;

	case CMARK_NODE_LINK:
		if (is_autolink(node)) {
			if (entering) {
				lit(state, "<", false);
				if (strncmp(cmark_node_get_url(node),
				            "mailto:", 7) == 0) {
					lit(state,
					    (char *)cmark_node_get_url(node) + 7,
					    false);
				} else {
					lit(state,
					    (char *)cmark_node_get_url(node),
					    false);
				}
				lit(state, ">", false);
				// return signal to skip contents of node...
				return 0;
			}
		} else {
			if (entering) {
				lit(state, "[", false);
			} else {
				lit(state, "](", false);
				out(state,
				    cmark_chunk_literal(cmark_node_get_url(node)),
				    false, URL);
				title = cmark_node_get_title(node);
				if (title && strlen(title) > 0) {
					lit(state, " \"", true);
					out(state, cmark_chunk_literal(title),
					    false, TITLE);
					lit(state, "\"", false);
				}
				lit(state, ")", false);
			}
		}
		break;

	case CMARK_NODE_IMAGE:
		if (entering) {
			lit(state, "![", false);
		} else {
			lit(state, "](", false);
			out(state, cmark_chunk_literal(cmark_node_get_url(node)), false, URL);
			title = cmark_node_get_title(node);
			if (title && strlen(title) > 0) {
				lit(state, " \"", true);
				out(state, cmark_chunk_literal(title), false, TITLE);
				lit(state, "\"", false);
			}
			lit(state, ")", false);
		}
		break;

	default:
		assert(false);
		break;
	}

	return 1;
}

char *cmark_render_commonmark(cmark_node *root, int options, int width)
{
	char *result;
	cmark_strbuf commonmark = GH_BUF_INIT;
	cmark_strbuf prefix = GH_BUF_INIT;
	if (CMARK_OPT_HARDBREAKS & options) {
		width = 0;
	}
	struct render_state state = {
		options, &commonmark, &prefix, 0, width,
		0, 0, true, false, false
	};
	cmark_node *cur;
	cmark_event_type ev_type;
	cmark_iter *iter = cmark_iter_new(root);

	while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
		cur = cmark_iter_get_node(iter);
		if (!S_render_node(cur, ev_type, &state)) {
			// a false value causes us to skip processing
			// the node's contents.  this is used for
			// autolinks.
			cmark_iter_reset(iter, cur, CMARK_EVENT_EXIT);
		}
	}
	result = (char *)cmark_strbuf_detach(&commonmark);

	cmark_strbuf_free(&prefix);
	cmark_iter_free(iter);
	return result;
}