/**
 * \file
 *
 * \brief Journal service based on dataflash
 *
 * This is a journal service which can be used to enter journal entries in a
 * dataflash and reading back the latest entry.
 *
 * - Compiler:           IAR EWAVR32 and GNU GCC for AVR32
 * - Supported devices:  All devices
 * - AppNote:
 *
 * \author               Atmel Corporation: http://www.atmel.com \n
 *                       Support and FAQ: http://support.atmel.no/
 *
 * \page License
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. The name of Atmel may not be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * 4. This software may only be redistributed and used in connection with an
 * Atmel AVR product.
 *
 * THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE
 * EXPRESSLY AND SPECIFICALLY DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */

#include <debug.h>

#include <block/journal.h>
#include <checksum/crc8.h>
#include <dmapool.h>
#include <workqueue.h>
#include <app/workqueue.h>
#include <string.h>
#include <status-codes.h>

#define JOURNAL_MAGIC0		0x42
#define JOURNAL_CARD0_MAGIC	0x1234
#define JOURNAL_CARD1_MAGIC	0x5678

enum journal_search_state {
	JOURNAL_SEARCH_STARTING,
	JOURNAL_SEARCH_FOR_BLOCK,
	JOURNAL_SEARCH_FOR_ENTRY,
};

#define JOURNAL_ENTRY_FULL_LENGTH	sizeof(struct journal_entry)
/* Length of the first part of a journal entry (ie. without the
 * committed card magics
 */
#define JOURNAL_ENTRY_START_LENGTH	(sizeof(struct journal_entry) - 4)

/**
 * \brief Find next valid journal entry address
 *
 * This function will return the next valid journal entry address. Usually it
 * will jsut increment the current address with the length of one entry, but
 * if the next entry will cross a page boundary, a small offset will be added
 * to allign it with the boundary.
 *
 * \param media      Journal descriptor
 * \param prev_addr  Previous address
 *
 * \return Next address
 */
static unsigned int journal_get_next_address(
		struct journal *media, unsigned int prev_addr)
{
	unsigned int next_addr = prev_addr + JOURNAL_ENTRY_FULL_LENGTH;

	/* If the next entry is going to cross a page boundary, move it to
	 * the start of the next page.
	 */
	if (dataflash_get_page_offset(media->storage, next_addr)
			+ JOURNAL_ENTRY_FULL_LENGTH
			> dataflash_get_page_size(media->storage)) {
		next_addr = (dataflash_get_page_index(media->storage,
					next_addr) + 1)
				* dataflash_get_page_size(media->storage);
	}
	return next_addr;
}

static void journal_open_entry_done(struct dataflash_request *request,
		void *context)
{
	struct journal *media = context;

	if (request->status != STATUS_OK) {
		media->status = JOURNAL_ERROR;
		dbg_error("Journal ERROR: Dataflash error\n");
	} else if (media->current_entry) {
		media->status = JOURNAL_ENTRY_OPEN;
	} else {
		media->status = JOURNAL_IDLE;
	}

	if (media->flags & JOURNAL_EMPTY) {
		media->flags &= ~JOURNAL_EMPTY;
	} else if (dataflash_get_block_index(media->storage,
				media->last_location)
			!= dataflash_get_block_index(media->storage,
				media->current_location)) {
		/* First entry started in new block, erase previous block */
		dbg_info("Journal: Done with block %x, erasing\n",
			dataflash_get_block_index(media->storage,
				media->last_location));

		media->erase_request.address = dataflash_get_block_index(
				media->storage,
				media->last_location);
		dataflash_erase_blocks(&media->erase_request);
	}

	media->last_location = media->current_location;
	media->current_location = journal_get_next_address(media,
			media->last_location);

	if (media->current_location + JOURNAL_ENTRY_FULL_LENGTH >
			media->start + media->length) {
		/* Next entry will not fit in journal space. Rewind to start.
		 * We do not need to worry about erasing the used block here,
		 * that will be handled when the first entry is written in the
		 * new block
		 */
		media->current_location = media->start;
	}

	if (media->callback)
		media->callback(media, media->current_entry, media->context);
}

/**
 * \brief Store journal entry
 *
 * This function will store the given journal entry to non-volatile storage
 * (dataflash). As soon as the entry has been securely stored, the given
 * callback will be called. Both journal_init() and journal_get_last_entry()
 * must have been called before this function can be used.
 *
 * \param media         Journal media used for storage
 * \param entry         The journal entry to store
 * \param entry_stored  Callback function called after storing entry
 * \param context       User defined data passed to the callback
 */
void journal_open_entry(
		struct journal *media,
		struct journal_entry *entry,
		journal_callback_t entry_stored,
		void *context)
{
	struct dataflash_request *request;

	assert(media);
	assert(entry);
	assert(entry->magic0 != 0xffff);
	assert(media->status == JOURNAL_IDLE);
	media->status = JOURNAL_BUSY;

	request = &media->request;
	media->current_entry = entry;
	media->callback = entry_stored;
	media->context = context;

	buffer_init_tx(&media->entry_buffer, entry,
			JOURNAL_ENTRY_START_LENGTH);

	request->address = media->current_location;
	request->request_done = journal_open_entry_done;

	dataflash_write_array(request);
}

static void journal_clean_entry_done(struct dataflash_request *request,
		void *context)
{
	dma_free(request->data->addr.ptr, sizeof(struct journal_entry));
	journal_open_entry_done(request, context);
}

/**
 * \brief Clean the last entry in the journal
 *
 * This function will clean the last entry in the journal by writing a
 * new dummy entry with size 0 and both cards committed. Used after
 * successfully doing a replay, or after deciding not to do a replay
 * despite the journal indicating one of the cards being dirty.
 *
 * This function is necessary because a dirty journal will become stale
 * as soon as the system allows an unjournaled write to any of the
 * devices in the array (for example, if there's only one device in the
 * array, journaling is pointless), and must not be replayed on the next
 * power-up.
 *
 * \param media         Journal media used for storage
 * \param entry_cleaned Callback function called after cleaning
 * \param context       User defined data passed to the callback
 */
void journal_clean_last_entry(
		struct journal *media,
		journal_callback_t entry_cleaned,
		void *context)
{
	struct journal_entry	*entry;
	struct dataflash_request *request;
	phys_addr_t		entry_phys;

	assert(media);
	assert(media->status == JOURNAL_IDLE);

	entry = dma_alloc(&entry_phys, sizeof(struct journal_entry));
	if (!entry) {
		dbg_error("journal: failed to allocate dummy entry\n");
		if (entry_cleaned)
			entry_cleaned(media, NULL, context);
		return;
	}

	media->status = JOURNAL_BUSY;
	buffer_init_tx_mapped(&media->entry_buffer, entry, entry_phys,
			sizeof(struct journal_entry));

	journal_prepare_entry(entry, 0, 0, 0);

	request = &media->request;
	media->current_entry = NULL;
	media->callback = entry_cleaned;
	media->context = context;

	request->address = media->current_location;
	request->request_done = journal_clean_entry_done;

	dataflash_write_array(request);
}

static void journal_commit_card_done(
		struct dataflash_request *request,
		void *context)
{
	struct journal *media = context;

	if (request->status != STATUS_OK) {
		media->status = JOURNAL_ERROR;
		dbg_error("Journal ERROR: Dataflash error\n");
	} else if (media->flags & JOURNAL_CARD0_COMMITTED &&
			media->flags & JOURNAL_CARD1_COMMITTED) {
		media->flags &= ~(JOURNAL_CARD0_COMMITTED
				| JOURNAL_CARD1_COMMITTED);
		media->status = JOURNAL_IDLE;
	} else {
		media->status = JOURNAL_ENTRY_OPEN;
	}

	if (media->callback)
		media->callback(media, media->current_entry, media->context);
}

/**
 * \brief Journal entry commited for a card
 *
 * This function will store the given journal entry to non-volatile storage
 * (dataflash). As soon as the entry has been securely stored, the given
 * callback will be called. The driver will internally calculate a checksum of
 * the entry enabling it to verify the read out when the entry is retrieved.
 *
 * \param media          Journal media used for storage
 * \param entry          The journal entry to update
 * \param card_nr        The card to commit
 * \param card_commited  Callback function called after commiting the card
 * \param context        User defined data passed to the callback
 */
void journal_commit_card(
		struct journal *media,
		struct journal_entry *entry,
		unsigned int card_nr,
		journal_callback_t card_committed,
		void *context)
{
	struct dataflash_request *request;

	assert(media);
	assert(entry);
	assert(entry == media->current_entry);
	assert(card_nr <= 1);
	assert(media->status == JOURNAL_ENTRY_OPEN);

	media->status = JOURNAL_BUSY;
	request = &media->request;
	media->callback = card_committed;
	media->context = context;

	if (card_nr == 0) {
		buffer_init_tx(&media->entry_buffer,
				&entry->committed_card0_magic, 2);
		request->address = media->last_location +
				JOURNAL_ENTRY_START_LENGTH;
		media->flags |= JOURNAL_CARD0_COMMITTED;
	} else if (card_nr == 1) {
		buffer_init_tx(&media->entry_buffer,
				&entry->committed_card1_magic, 2);
		request->address = media->last_location +
				JOURNAL_ENTRY_START_LENGTH + 2;
		media->flags |= JOURNAL_CARD1_COMMITTED;
	}

	request->request_done = journal_commit_card_done;
	dataflash_write_array(request);
}

/**
 * \brief Close current entry without committing all cards
 *
 * This function will close the current journal entry, without committing any
 * further cards. When the entry is closed, the journal is immediately ready
 * to open a new entry. Do not close an open entry while a open entry or commit
 * card operation is in progress.
 *
 * \param media          Journal media used for storage
 */
void journal_close_entry(struct journal *media)
{
	assert(media);

	media->flags &= ~(JOURNAL_CARD0_COMMITTED
			| JOURNAL_CARD1_COMMITTED);
	media->status = JOURNAL_IDLE;
}

static void journal_get_last_entry_done(
		struct dataflash_request *request,
		void *context)
{
	struct journal *media = context;

	if (request->status != STATUS_OK) {
		media->status = JOURNAL_ERROR;
		dbg_error("Journal ERROR: Dataflash error\n");
	} else {
		media->status = JOURNAL_IDLE;
	}

	if (media->callback) {
		if (media->flags & JOURNAL_EMPTY)
			media->callback(media, NULL, media->context);
		else
			media->callback(media, media->current_entry,
					media->context);
	}
}

static void journal_read_last_entry(
		struct journal *media)
{
	buffer_init_rx(&media->entry_buffer, media->current_entry,
			JOURNAL_ENTRY_FULL_LENGTH);

	media->request.address = media->last_location;
	media->request.request_done = journal_get_last_entry_done;
	dataflash_read_array(&media->request);
}

static void journal_run_find_last_entry(
		struct dataflash_request *request,
		void *context)
{
	struct journal *media = context;

	workqueue_add_item(&journal_workqueue, &media->workqueue_item);
}

static void journal_reerase_previous_block(struct journal *media)
{
	unsigned int current_block;
	unsigned int first_block;
	unsigned int last_block;

	/* Return if our entry is not the first in a block */
	if (dataflash_get_block_index(media->storage, media->last_location)
			== dataflash_get_block_index(media->storage,
					media->last_location - 1)) {
		return;
	}
	/* Return if card0 was committed */
	if (media->current_entry->committed_card0_magic
			== JOURNAL_CARD0_MAGIC)
		return;
	/* If we get here the last entry is the first on a block and we lost
	 * power before card0 was committed. This could imply that the
	 * previous block did not get properly erased; we will erase it again
	 * just to be sure.
	 */
	current_block = dataflash_get_block_index(
			media->storage,
			media->last_location);
	first_block = dataflash_get_block_index(
			media->storage,
			media->start);
	last_block = dataflash_get_block_index(
			media->storage,
			media->start + media->length - 1);
	if (current_block == first_block)
		media->erase_request.address = last_block;
	else
		media->erase_request.address = current_block - 1;
	dataflash_erase_blocks(&media->erase_request);
}

/**
 * \brief Search dataflash for last journal entry
 *
 * This function will search through the dataflash for the last journal entry
 * stored. The last_location and current_location pointers will be updated.
 *
 * The function interface is set us a dataflash callback, simplifying
 * recursive operation.
 *
 * \param data   Data pointer returned with the callback, will point to
 *               the journal structure.
 */
static void journal_find_last_entry(void *data)
{
	struct journal *media = data;

	/* Request is NULL when the read starts, not-NULL for each later read */
	switch (media->search_mode) {
	case JOURNAL_SEARCH_STARTING:
		media->current_location = media->start;
		media->search_mode = JOURNAL_SEARCH_FOR_BLOCK;
		break;
	case JOURNAL_SEARCH_FOR_BLOCK:
		if (media->magic_number == 0xffff) {
			/* Block is empty, jump to next */
			media->current_location += dataflash_get_block_size(
					media->storage);
			break;
		} else {
			/* Found used block, scan for last entry */
			media->search_mode = JOURNAL_SEARCH_FOR_ENTRY;
			media->flags &= ~JOURNAL_EMPTY;
			/* Fall through */
		}
	case JOURNAL_SEARCH_FOR_ENTRY:
		if (media->magic_number == 0xffff) {
			/* Found end of journal */
			journal_read_last_entry(media);
			journal_reerase_previous_block(media);
			return;
		} else {
			media->last_location = media->current_location;
			media->current_location = journal_get_next_address(
					media, media->current_location);
		}
		break;
	default:
		unhandled_case(media->search_mode);
	}

	if (media->current_location + JOURNAL_ENTRY_FULL_LENGTH
			> media->start + media->length) {
		/* Last entry is at end of media, we should write the next
		 * to the start */
		media->current_location = media->start;
		journal_read_last_entry(media);
		return;
	}

	buffer_init_rx(&media->entry_buffer, &media->magic_number, 2);
	media->request.address = media->current_location;
	dataflash_read_array(&media->request);
}

/**
 * \brief Retrieve last complete journal entry
 *
 * This function will retrieve the last journal entry stored on the
 * non-volatile storage (dataflash). This entry will then be passed on to
 * the given callback function. Afterwards journal_entry_card_is_intact() can
 * be used to investigate the retrieved entry.
 *
 * \param media           Journal media used for storage
 * \param entry           Pointer to where the retrieved data can be stored
 * \param entry_retrieved Callback function called after retrieving entry
 * \param context         User defined data passed to the callback
 */
void journal_get_last_entry(
		struct journal *media,
		struct journal_entry *entry,
		journal_callback_t entry_retrieved,
		void *context)
{
	assert(media);
	assert(media->status == JOURNAL_IDLE
			|| media->status == JOURNAL_END_NOT_LOCALIZED);

	media->status = JOURNAL_BUSY;
	media->callback = entry_retrieved;
	media->context = context;
	media->current_entry = entry;
	/* Assume journal is empty until we find an entry */
	media->flags |= JOURNAL_EMPTY;
	media->search_mode = JOURNAL_SEARCH_STARTING;

	media->request.request_done = journal_run_find_last_entry;
	journal_find_last_entry(media);
}

/**
 * \brief Prepare a new journal entry
 *
 * This function will prepare a journal entry for a new write operation. The
 * entry should be opened with journal_open_entry() before starting the write,
 * and each card should be committed with journal_commit_card() after it has
 * been successfully written.
 *
 * \param entry           Pointer to journal entry structure
 * \param card_id         ID of the card being written
 * \param block           Address written on the card
 * \param length          Length of the write operation
 */
void journal_prepare_entry(
		struct journal_entry *entry,
		uint32_t card_id,
		uint32_t block,
		uint16_t length)
{
	uint8_t journal_checksum;

	assert(entry);

	entry->card_id = card_id;
	entry->block = block;
	entry->length = length;

	/* Store checksum of non-magic part of journal entry */
	journal_checksum = checksum_crc8((uint8_t *)&entry->length, 10);
	entry->magic0 = JOURNAL_MAGIC0 << 8 | journal_checksum;
	entry->committed_card0_magic = JOURNAL_CARD0_MAGIC;
	entry->committed_card1_magic = JOURNAL_CARD1_MAGIC;
}

/**
 * \brief Check a journal entry to see if a card was written correctly
 *
 * This function will check the fields of the journal entry given to it, and
 * return the state of one of the cards. If the journal entry indicates that
 * a write to the card has started, but not completed, the function will
 * return false. The position and length fields in the journal entry will
 * provide information about which region of the card that might be
 * corrupted.
 *
 * \param entry       Pointer to journal entry structure
 * \param card_nr     Number of the card to check
 * \return true       Whole card is intact
 * \return false      The area defined in entry might be corrupt
 */
bool journal_entry_card_is_intact(
		struct journal_entry *entry,
		unsigned int card_nr)
{
	uint8_t checksum;

	assert(entry);
	assert(card_nr <= 1);

	checksum = checksum_crc8((uint8_t *)&entry->length, 10);

	if (entry->magic0 != (JOURNAL_MAGIC0 << 8 | checksum)) {
		/* Journal entry not opened successfully so no data
		 * has been written to cards: Both cards are intact
		 */
		return true;
	} else if (entry->committed_card0_magic != JOURNAL_CARD0_MAGIC) {
		/* Card0 has not been committed and might be corrupt,
		 * no data written to card1; copy from card1 to card0 */
		if (card_nr == 0)
			return false;
		else
			return true;
	} else if (entry->committed_card1_magic != JOURNAL_CARD1_MAGIC) {
		/* Card0 has been successfully written, but card1 might
		 * be corrupt; copy from card0 to card1
		 */
		if (card_nr == 0)
			return true;
		else
			return false;
	} else {
		/* Both cards have been successfully written */
		return true;
	}
}

/**
 * \brief Check the consistency of a journal entry
 *
 * This function will check the consistency of a journal entry. If a entry is
 * found not to be consistent, something is very wrong; most probably we have
 * a failing storage media. Getting disturbed during write or losing power
 * will never cause an inconsistent journal entry.
 *
 * \param entry       Pointer to journal entry structure
 * \return true       Entry seems consistent
 * \return false      Entry is not consistent; storage media probably faulty
 */
bool journal_entry_is_consistent(struct journal_entry *entry)
{
	uint8_t checksum;

	assert(entry);

	checksum = checksum_crc8((uint8_t *)&entry->length, 10);

	/* If the checksum does not match, there should not be any data in
	 * any of the commit fields. If we find anything here without a
	 * correct checksum, something is very wrong; most probably the
	 * storage media is failing...
	 */
	if (entry->magic0 != (JOURNAL_MAGIC0 << 8 | checksum)) {
		if (entry->committed_card0_magic != 0xffff ||
				entry->committed_card1_magic != 0xffff)
			return false;
	}
	return true;
}

/**
 * \brief Initialize a journal
 *
 * This function will initialize a journal and set up the journal struct.
 * After the journal has been initialized, the journal_get_last_entry()
 * function can be used to get retrieve the last entry written to the
 * journal.
 *
 * \param journal       The journal struct
 * \param storage       The dataflash used for storage
 * \param start_addr    First flash address used by the journal
 * \param length        Flash area dedicated to the journal
 */
void journal_init(
		struct journal *media,
		struct dataflash_device *storage,
		unsigned int start_addr,
		unsigned int length)
{
	assert(media);
	assert(storage);

	memset(media, 0, sizeof(struct journal));
	media->status = JOURNAL_END_NOT_LOCALIZED;

	if (dataflash_get_block_index(storage, start_addr - 1)
			== dataflash_get_block_index(storage, start_addr)) {
		/* Not aligned with block boundary, start at next block */
		start_addr = dataflash_get_block_size(storage) *
			(dataflash_get_block_index(storage, start_addr) + 1);
		dbg_warning("Journal: Moving start address to %x to align "
				"with block boundary (block %x)\n",
				start_addr,
				dataflash_get_block_index(storage,
					start_addr));
	}
	assert(start_addr + length <= dataflash_get_device_size(storage));

	media->start = start_addr;
	media->length = length;
	media->storage = storage;
	workqueue_init_item(&media->workqueue_item,
			journal_find_last_entry, media);

	/* Set up the request for later use */
	media->request.data = &media->entry_buffer;
	media->request.device = media->storage;
	media->request.context = media;

	media->erase_request.device = media->storage;
	media->erase_request.length = 1;
}
