/* Dhara - NAND flash management layer
 * Copyright (C) 2013 Daniel Beer <dlbeer@gmail.com>
 *
 * Permission to use, copy, modify, and/or 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 <string.h>
#include "bytes.h"
#include "map.h"

#define DHARA_RADIX_DEPTH	(sizeof(dhara_sector_t) << 3)

static inline dhara_sector_t d_bit(int depth)
{
	return ((dhara_sector_t)1) << (DHARA_RADIX_DEPTH - depth - 1);
}

/************************************************************************
 * Metadata/cookie layout
 */

static inline void ck_set_count(uint8_t *cookie, dhara_sector_t count)
{
	dhara_w32(cookie, count);
}

static inline dhara_sector_t ck_get_count(const uint8_t *cookie)
{
	return dhara_r32(cookie);
}

static inline void meta_clear(uint8_t *meta)
{
	memset(meta, 0xff, DHARA_META_SIZE);
}

static inline dhara_sector_t meta_get_id(const uint8_t *meta)
{
	return dhara_r32(meta);
}

static inline void meta_set_id(uint8_t *meta, dhara_sector_t id)
{
	dhara_w32(meta, id);
}

static inline dhara_page_t meta_get_alt(const uint8_t *meta, int level)
{
	return dhara_r32(meta + 4 + (level << 2));
}

static inline void meta_set_alt(uint8_t *meta, int level, dhara_page_t alt)
{
	dhara_w32(meta + 4 + (level << 2), alt);
}

/************************************************************************
 * Public interface
 */

void dhara_map_init(struct dhara_map *m, const struct dhara_nand *n,
		    uint8_t *page_buf, uint8_t gc_ratio)
{
	if (!gc_ratio)
		gc_ratio = 1;

	dhara_journal_init(&m->journal, n, page_buf);
	m->gc_ratio = gc_ratio;
}

int dhara_map_resume(struct dhara_map *m, dhara_error_t *err)
{
	if (dhara_journal_resume(&m->journal, err) < 0) {
		m->count = 0;
		return -1;
	}

	m->count = ck_get_count(dhara_journal_cookie(&m->journal));
	return 0;
}

void dhara_map_clear(struct dhara_map *m)
{
	if (m->count) {
		m->count = 0;
		dhara_journal_clear(&m->journal);
	}
}

dhara_sector_t dhara_map_capacity(const struct dhara_map *m)
{
	const dhara_sector_t cap = dhara_journal_capacity(&m->journal);
	const dhara_sector_t reserve = cap / (m->gc_ratio + 1);
	const dhara_sector_t safety_margin =
		DHARA_MAX_RETRIES << m->journal.nand->log2_ppb;

	if (reserve + safety_margin >= cap)
		return 0;

	return cap - reserve - safety_margin;
}

/* Trace the path from the root to the given sector, emitting
 * alt-pointers and alt-full bits in the given metadata buffer. This
 * also returns the physical page containing the given sector, if it
 * exists.
 *
 * If the page can't be found, a suitable path will be constructed
 * (containing PAGE_NONE alt-pointers), and DHARA_E_NOT_FOUND will be
 * returned.
 */
static int trace_path(struct dhara_map *m, dhara_sector_t target,
		      dhara_page_t *loc, uint8_t *new_meta,
		      dhara_error_t *err)
{
	uint8_t meta[DHARA_META_SIZE];
	int depth = 0;
	dhara_page_t p = dhara_journal_root(&m->journal);

	if (new_meta)
		meta_set_id(new_meta, target);

	if (p == DHARA_PAGE_NONE)
		goto not_found;

	if (dhara_journal_read_meta(&m->journal, p, meta, err) < 0)
		return -1;

	while (depth < DHARA_RADIX_DEPTH) {
		const dhara_sector_t id = meta_get_id(meta);

		if (id == DHARA_SECTOR_NONE)
			goto not_found;

		if ((target ^ id) & d_bit(depth)) {
			if (new_meta)
				meta_set_alt(new_meta, depth, p);

			p = meta_get_alt(meta, depth);
			if (p == DHARA_PAGE_NONE) {
				depth++;
				goto not_found;
			}

			if (dhara_journal_read_meta(&m->journal, p,
						    meta, err) < 0)
				return -1;
		} else {
			if (new_meta)
				meta_set_alt(new_meta, depth,
					meta_get_alt(meta, depth));
		}

		depth++;
	}

	if (loc)
		*loc = p;

	return 0;

not_found:
	if (new_meta) {
		while (depth < DHARA_RADIX_DEPTH)
			meta_set_alt(new_meta, depth++, DHARA_SECTOR_NONE);
	}

	dhara_set_error(err, DHARA_E_NOT_FOUND);
	return -1;
}

int dhara_map_find(struct dhara_map *m, dhara_sector_t target,
		   dhara_page_t *loc, dhara_error_t *err)
{
	return trace_path(m, target, loc, NULL, err);
}

int dhara_map_read(struct dhara_map *m, dhara_sector_t s,
		   uint8_t *data, dhara_error_t *err)
{
	const struct dhara_nand *n = m->journal.nand;
	dhara_error_t my_err;
	dhara_page_t p;

	if (dhara_map_find(m, s, &p, &my_err) < 0) {
		if (my_err == DHARA_E_NOT_FOUND) {
			memset(data, 0xff, 1 << n->log2_page_size);
			return 0;
		}

		dhara_set_error(err, my_err);
		return -1;
	}

	return dhara_nand_read(n, p, 0, 1 << n->log2_page_size, data, err);
}

/* Check the given page. If it's garbage, do nothing. Otherwise, rewrite
 * it at the front of the map. Return raw errors from the journal (do
 * not perform recovery).
 */
static int raw_gc(struct dhara_map *m, dhara_page_t src,
		  dhara_error_t *err)
{
	dhara_sector_t target;
	dhara_page_t current;
	dhara_error_t my_err;
	uint8_t meta[DHARA_META_SIZE];

	if (dhara_journal_read_meta(&m->journal, src, meta, err) < 0)
		return -1;

	/* Is the page just filler/garbage? */
	target = meta_get_id(meta);
	if (target == DHARA_SECTOR_NONE)
		return 0;

	/* Find out where the sector once represented by this page
	 * currently resides (if anywhere).
	 */
	if (trace_path(m, target, &current, meta, &my_err) < 0) {
		if (my_err == DHARA_E_NOT_FOUND)
			return 0;

		dhara_set_error(err, my_err);
		return -1;
	}

	/* Is this page still the most current representative? If not,
	 * do nothing.
	 */
	if (current != src)
		return 0;

	/* Rewrite it at the front of the journal with updated metadata */
	ck_set_count(dhara_journal_cookie(&m->journal), m->count);
	if (dhara_journal_copy(&m->journal, src, meta, err) < 0)
		return -1;

	return 0;
}

static int pad_queue(struct dhara_map *m, dhara_error_t *err)
{
	dhara_page_t p = dhara_journal_root(&m->journal);
	uint8_t root_meta[DHARA_META_SIZE];

	ck_set_count(dhara_journal_cookie(&m->journal), m->count);

	if (p == DHARA_PAGE_NONE)
		return dhara_journal_enqueue(&m->journal, NULL, NULL, err);

	if (dhara_journal_read_meta(&m->journal, p, root_meta, err) < 0)
		return -1;

	return dhara_journal_copy(&m->journal, p, root_meta, err);
}

/* Attempt to recover the journal */
static int try_recover(struct dhara_map *m, dhara_error_t cause,
		       dhara_error_t *err)
{
	int restart_count = 0;

	if (cause != DHARA_E_RECOVER) {
		dhara_set_error(err, cause);
		return -1;
	}

	while (dhara_journal_in_recovery(&m->journal)) {
		dhara_page_t p = dhara_journal_next_recoverable(&m->journal);
		dhara_error_t my_err;
		int ret;

		if (p == DHARA_PAGE_NONE)
			ret = pad_queue(m, &my_err);
		else
			ret = raw_gc(m, p, &my_err);

		if (ret < 0) {
			if (my_err != DHARA_E_RECOVER) {
				dhara_set_error(err, my_err);
				return -1;
			}

			if (restart_count >= DHARA_MAX_RETRIES) {
				dhara_set_error(err, DHARA_E_TOO_BAD);
				return -1;
			}

			restart_count++;
		}
	}

	return 0;
}

static int auto_gc(struct dhara_map *m, dhara_error_t *err)
{
	int i;

	if (dhara_journal_size(&m->journal) < dhara_map_capacity(m))
		return 0;

	for (i = 0; i <= m->gc_ratio; i++)
		if (dhara_map_gc(m, err) < 0)
			return -1;

	return 0;
}

static int prepare_write(struct dhara_map *m, dhara_sector_t dst,
			 uint8_t *meta, dhara_error_t *err)
{
	dhara_error_t my_err;

	if (auto_gc(m, err) < 0)
		return -1;

	if (trace_path(m, dst, NULL, meta, &my_err) < 0) {
		if (my_err != DHARA_E_NOT_FOUND) {
			dhara_set_error(err, my_err);
			return -1;
		}

		if (m->count >= dhara_map_capacity(m)) {
			dhara_set_error(err, DHARA_E_MAP_FULL);
			return -1;
		}

		m->count++;
	}

	ck_set_count(dhara_journal_cookie(&m->journal), m->count);
	return 0;
}

int dhara_map_write(struct dhara_map *m, dhara_sector_t dst,
		    const uint8_t *data, dhara_error_t *err)
{
	for (;;) {
		uint8_t meta[DHARA_META_SIZE];
		dhara_error_t my_err;
		const dhara_sector_t old_count = m->count;

		if (prepare_write(m, dst, meta, err) < 0)
			return -1;

		if (!dhara_journal_enqueue(&m->journal, data, meta, &my_err))
			break;

		m->count = old_count;

		if (try_recover(m, my_err, err) < 0)
			return -1;
	}

	return 0;
}

int dhara_map_copy_page(struct dhara_map *m, dhara_page_t src,
			dhara_sector_t dst, dhara_error_t *err)
{
	for (;;) {
		uint8_t meta[DHARA_META_SIZE];
		dhara_error_t my_err;
		const dhara_sector_t old_count = m->count;

		if (prepare_write(m, dst, meta, err) < 0)
			return -1;

		if (!dhara_journal_copy(&m->journal, src, meta, &my_err))
			break;

		m->count = old_count;

		if (try_recover(m, my_err, err) < 0)
			return -1;
	}

	return 0;
}

int dhara_map_copy_sector(struct dhara_map *m, dhara_sector_t src,
			  dhara_sector_t dst, dhara_error_t *err)
{
	dhara_error_t my_err;
	dhara_page_t p;

	if (dhara_map_find(m, src, &p, &my_err) < 0) {
		if (my_err == DHARA_E_NOT_FOUND)
			return dhara_map_trim(m, dst, err);

		dhara_set_error(err, my_err);
		return -1;
	}

	return dhara_map_copy_page(m, p, dst, err);
}

static int try_delete(struct dhara_map *m, dhara_sector_t s,
		      dhara_error_t *err)
{
	dhara_error_t my_err;
	uint8_t meta[DHARA_META_SIZE];
	dhara_page_t alt_page;
	uint8_t alt_meta[DHARA_META_SIZE];
	int level = DHARA_RADIX_DEPTH - 1;
	int i;

	if (trace_path(m, s, NULL, meta, &my_err) < 0) {
		if (my_err == DHARA_E_NOT_FOUND)
			return 0;

		dhara_set_error(err, my_err);
		return -1;
	}

	/* Select any of the closest cousins of this node which are
	 * subtrees of at least the requested order.
	 */
	while (level >= 0) {
		alt_page = meta_get_alt(meta, level);
		if (alt_page != DHARA_PAGE_NONE)
			break;
		level--;
	}

	/* Special case: deletion of last sector */
	if (level < 0) {
		m->count = 0;
		dhara_journal_clear(&m->journal);
		return 0;
	}

	/* Rewrite the cousin with an up-to-date path which doesn't
	 * point to the original node.
	 */
	if (dhara_journal_read_meta(&m->journal, alt_page, alt_meta, err) < 0)
		return -1;

	meta_set_id(meta, meta_get_id(alt_meta));

	meta_set_alt(meta, level, DHARA_PAGE_NONE);
	for (i = level + 1; i < DHARA_RADIX_DEPTH; i++)
		meta_set_alt(meta, i, meta_get_alt(alt_meta, i));

	meta_set_alt(meta, level, DHARA_PAGE_NONE);

	ck_set_count(dhara_journal_cookie(&m->journal), m->count - 1);
	if (dhara_journal_copy(&m->journal, alt_page, meta, err) < 0)
		return -1;

	m->count--;
	return 0;
}

int dhara_map_trim(struct dhara_map *m, dhara_sector_t s, dhara_error_t *err)
{
	for (;;) {
		dhara_error_t my_err;

		if (auto_gc(m, err) < 0)
			return -1;

		if (!try_delete(m, s, &my_err))
			break;

		if (try_recover(m, my_err, err) < 0)
			return -1;
	}

	return 0;
}

int dhara_map_sync(struct dhara_map *m, dhara_error_t *err)
{
	while (!dhara_journal_is_clean(&m->journal)) {
		dhara_page_t p = dhara_journal_peek(&m->journal);
		dhara_error_t my_err;
		int ret;

		if (p == DHARA_PAGE_NONE) {
			ret = pad_queue(m, &my_err);
		} else {
			ret = raw_gc(m, p, &my_err);
			if (!ret)
				dhara_journal_dequeue(&m->journal);
		}

		if ((ret < 0) && (try_recover(m, my_err, err) < 0))
			return -1;
	}

	return 0;
}

int dhara_map_gc(struct dhara_map *m, dhara_error_t *err)
{
	if (!m->count)
		return 0;

	for (;;) {
		dhara_page_t tail = dhara_journal_peek(&m->journal);
		dhara_error_t my_err;

		if (tail == DHARA_PAGE_NONE)
			break;

		if (!raw_gc(m, tail, &my_err)) {
			dhara_journal_dequeue(&m->journal);
			break;
		}

		if (try_recover(m, my_err, err) < 0)
			return -1;
	}

	return 0;
}
