#include "cmark.h"
#include "utf8.h"
#include "parser.h"
#include "references.h"
#include "inlines.h"
#include "chunk.h"

static unsigned int
refhash(const unsigned char *link_ref)
{
	unsigned int hash = 0;

	while (*link_ref)
		hash = (*link_ref++) + (hash << 6) + (hash << 16) - hash;

	return hash;
}

static void reference_free(cmark_reference *ref)
{
	if(ref != NULL) {
		free(ref->label);
		cmark_chunk_free(&ref->url);
		cmark_chunk_free(&ref->title);
		free(ref);
	}
}

// normalize reference:  collapse internal whitespace to single space,
// remove leading/trailing whitespace, case fold
// Return NULL if the reference name is actually empty (i.e. composed
// solely from whitespace)
static unsigned char *normalize_reference(cmark_chunk *ref)
{
	cmark_strbuf normalized = GH_BUF_INIT;
	unsigned char *result;

	if(ref == NULL)
		return NULL;

	if (ref->len == 0)
		return NULL;

	utf8proc_case_fold(&normalized, ref->data, ref->len);
	cmark_strbuf_trim(&normalized);
	cmark_strbuf_normalize_whitespace(&normalized);

	result = cmark_strbuf_detach(&normalized);
	assert(result);

	if (result[0] == '\0') {
		free(result);
		return NULL;
	}

	return result;
}

static void add_reference(cmark_reference_map *map, cmark_reference* ref)
{
	cmark_reference *t = ref->next = map->table[ref->hash % REFMAP_SIZE];

	while (t) {
		if (t->hash == ref->hash &&
		    !strcmp((char *)t->label, (char *)ref->label)) {
			reference_free(ref);
			return;
		}

		t = t->next;
	}

	map->table[ref->hash % REFMAP_SIZE] = ref;
}

void cmark_reference_create(cmark_reference_map *map, cmark_chunk *label, cmark_chunk *url,
                            cmark_chunk *title)
{
	cmark_reference *ref;
	unsigned char *reflabel = normalize_reference(label);

	/* empty reference name, or composed from only whitespace */
	if (reflabel == NULL)
		return;

	ref = (cmark_reference *)calloc(1, sizeof(*ref));
	if(ref != NULL) {
		ref->label = reflabel;
		ref->hash = refhash(ref->label);
		ref->url = cmark_clean_url(url);
		ref->title = cmark_clean_title(title);
		ref->next = NULL;

		add_reference(map, ref);
	}
}

// Returns reference if refmap contains a reference with matching
// label, otherwise NULL.
cmark_reference* cmark_reference_lookup(cmark_reference_map *map, cmark_chunk *label)
{
	cmark_reference *ref = NULL;
	unsigned char *norm;
	unsigned int hash;

	if (label->len > MAX_LINK_LABEL_LENGTH)
		return NULL;

	if (map == NULL)
		return NULL;

	norm = normalize_reference(label);
	if (norm == NULL)
		return NULL;

	hash = refhash(norm);
	ref = map->table[hash % REFMAP_SIZE];

	while (ref) {
		if (ref->hash == hash &&
		    !strcmp((char *)ref->label, (char *)norm))
			break;
		ref = ref->next;
	}

	free(norm);
	return ref;
}

void cmark_reference_map_free(cmark_reference_map *map)
{
	unsigned int i;

	if(map == NULL)
		return;

	for (i = 0; i < REFMAP_SIZE; ++i) {
		cmark_reference *ref = map->table[i];
		cmark_reference *next;

		while (ref) {
			next = ref->next;
			reference_free(ref);
			ref = next;
		}
	}

	free(map);
}

cmark_reference_map *cmark_reference_map_new(void)
{
	return (cmark_reference_map *)calloc(1, sizeof(cmark_reference_map));
}
