/**
 * \file
 *
 * \brief Block copy between block devices
 *
 * This file contains the functionality needed to copy a chunk of blocks
 * from one block device to another.
 *
 * - 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 <malloc.h>
#include <string.h>
#include <block/device.h>
#include <buffer.h>
#include <workqueue.h>
#include <app/workqueue.h>
#include <app/config_dmapool.h>
#include <block/copy.h>

#define BLOCK_COPY_BUF_SIZE	CONFIG_DMAPOOL_LARGE_OBJ_SIZE
#define BLOCK_COPY_MAX_BUFFERS	(CONFIG_DMAPOOL_NR_LARGE_OBJS / 2)

static void block_copy_read_blocks(void *data);

static void block_copy_complete(void *data)
{
	struct block_copy_request *req = data;

	if (req->completion_callback)
		req->completion_callback(req, req->total_blocks_copied,
				req->data);
}

static void block_copy_continue(void *data)
{
	struct block_copy_request *req = data;

	if (req->progress_callback)
		req->progress_callback(req, req->total_blocks_copied,
				req->data);

	workqueue_init_item(&req->work,
			block_copy_read_blocks, req);
	workqueue_add_item(&block_workqueue, &req->work);
}

static void block_copy_write_done(
		struct block_device *bdev,
		struct block_request *breq)
{
	struct block_copy_request *req = breq->context;
	uint32_t blocks_xfered = breq->bytes_xfered / req->block_size;

	if (breq->status) {
		dbg_error("block copy: Error during write, status: %d\n",
				breq->status);
		req->status = req->dest_status = breq->status;
		/* Schedule the completion callback to be executed */
		workqueue_init_item(&req->work,
				block_copy_complete, req);
		workqueue_add_item(&block_workqueue, &req->work);
		return;
	} else {
		dbg_verbose("block copy: Write completed: %zu blocks "
				"written\n", breq->bytes_xfered);
	}

	slist_move_to_tail(&req->buffer_list, &breq->buf_list);
	req->total_blocks_copied += blocks_xfered;

	if (req->total_blocks_copied < req->length)
		workqueue_init_item(&req->work, block_copy_continue, req);
	else
		workqueue_init_item(&req->work, block_copy_complete, req);

	workqueue_add_item(&block_workqueue, &req->work);
}

static void block_copy_write_blocks(void *data)
{
	struct block_copy_request *req = data;

	block_prepare_req(req->target, req->write_req,
			req->target_start + req->total_blocks_copied,
			req->blocks_allocated, BLK_OP_WRITE);
	slist_move_to_tail(&req->write_req->buf_list,
			&req->read_req->buf_list);
	block_submit_req(req->target, req->write_req);
}

static void block_copy_read_done(
		struct block_device *bdev,
		struct block_request *breq)
{
	struct block_copy_request *req = breq->context;

	if (breq->status) {
		dbg_error("block copy: Error during read, status: %d\n",
				breq->status);

		req->status = req->source_status = breq->status;

		/* Schedule the completion callback to be executed */
		workqueue_init_item(&req->work,
				block_copy_complete, req);
	} else {
		dbg_verbose("block copy: Read completed: %zu bytes read\n",
				breq->bytes_xfered);
		workqueue_init_item(&req->work, block_copy_write_blocks, req);
	}

	workqueue_add_item(&block_workqueue, &req->work);
}

static void block_copy_read_blocks(void *data)
{
	struct block_copy_request *req = data;
	struct buffer *buf;
	struct slist *buffer_list = &req->buffer_list;
	uint32_t blocks_remaining = req->length - req->total_blocks_copied;
	uint32_t excess_blocks;

	if (req->total_blocks_copied > 0) {
		/* Free excess buffers */
		while (req->blocks_allocated - req->blocks_pr_buf
				>= blocks_remaining) {
			buf = slist_pop_head(buffer_list,
					struct buffer, node);
			buffer_dma_free(buf, BLOCK_COPY_BUF_SIZE);
			req->blocks_allocated -= req->blocks_pr_buf;
		}

		/* If our buffers still are to big after removing the
		 * excess, we need to resize one of them to fit our data */
		if (req->blocks_allocated > blocks_remaining) {
			excess_blocks = req->blocks_allocated
				- blocks_remaining;
			buf = slist_peek_tail(buffer_list,
					struct buffer, node);
			buffer_resize(buf, BLOCK_COPY_BUF_SIZE
					- req->block_size * excess_blocks);
			req->blocks_allocated = blocks_remaining;
		}
	}

	block_prepare_req(req->source, req->read_req,
			req->source_start + req->total_blocks_copied,
			req->blocks_allocated, BLK_OP_READ);
	slist_move_to_tail(&req->read_req->buf_list, buffer_list);

	block_submit_req(req->source, req->read_req);
}

/**
 * \brief Allocate a block copy request and its buffers
 *
 * This function will allocate a block copy request with as many buffers as
 * necessary, up to the maximum available memory. If it is not able to allocate
 * the minimum memory needed, or the block size given is not supported, NULL is
 * returned.
 *
 * \param nr_blocks   The number of blocks to copy
 * \param block_size  The size of each block in bytes
 * \param data        Context data passed to callbacks
 * \return            The block copy request descriptor
 */
struct block_copy_request *block_copy_alloc(
		uint32_t nr_blocks,
		uint32_t block_size,
		void *data)
{
	struct block_copy_request *request;
	uint32_t alloc_size;
	struct buffer *buf;
	unsigned int i;

	assert(nr_blocks > 0);

	if (BLOCK_COPY_BUF_SIZE < block_size)
		return NULL;
	if (BLOCK_COPY_BUF_SIZE % block_size)
		return NULL;

	request = malloc(sizeof(struct block_copy_request));
	if (!request)
		return NULL;

	memset(request, 0, sizeof(struct block_copy_request));

	request->block_size = block_size;
	request->blocks_pr_buf = BLOCK_COPY_BUF_SIZE / block_size;

	request->data = data;
	request->length = nr_blocks;
	slist_init(&request->buffer_list);

	for (i = 0; i < BLOCK_COPY_MAX_BUFFERS; i++) {
		uint32_t blocks_remaining;

		/* We only allocate from the large object pool and resize
		 * the last buffer to match the size of our data */
		buf = buffer_dma_alloc(BLOCK_COPY_BUF_SIZE);
		if (!buf)
			break;

		slist_insert_tail(&request->buffer_list, &buf->node);
		blocks_remaining = request->length - request->blocks_allocated;
		alloc_size = request->blocks_pr_buf;
		request->blocks_allocated += alloc_size;

		if (alloc_size >= blocks_remaining) {
			if (alloc_size > blocks_remaining) {
				request->blocks_allocated
					-= alloc_size - blocks_remaining;
				alloc_size = blocks_remaining;
				buffer_resize(buf, alloc_size * block_size);
			}
			break;
		}
	}

	if (request->blocks_allocated == 0) {
		/* Give up if we could not allocate any buffers */
		free(request);
		return NULL;
	}

	return request;
}

/**
 * \brief Free block copy request
 *
 * This function will free all memory allocated to the block copy request,
 * including all allocated buffers and requests.
 *
 * \param request     Block copy request descriptor
 */
void block_copy_free(struct block_copy_request *request)
{
	struct buffer *buf;

	while (!slist_is_empty(&request->buffer_list)) {
		buf = slist_pop_head(&request->buffer_list,
				struct buffer, node);
		/* All our buffers are allocated from the large object
		 * pool, even those that are smaller */
		buffer_dma_free(buf, BLOCK_COPY_BUF_SIZE);
	}

	if (request->read_req)
		block_free_request(request->source, request->read_req);
	if (request->write_req)
		block_free_request(request->target, request->write_req);

	free(request);
}

/**
 * \brief Set source device and blocks for the block copy
 *
 * This function will set the source device and the address of the first
 * block to copy from the source in the block copy request descriptor.
 *
 * \param req     Block copy request descriptor
 * \param dev     Source device
 * \param lba     Address of first source block
 * \return        0 on success, negative error code on failure
 */
int block_copy_set_source(
		struct block_copy_request *req,
		struct block_device *dev,
		block_addr_t lba)
{
	assert(req);
	assert(dev);

	if (req->block_size != dev->block_size)
		return -BLOCK_COPY_BLOCK_SIZE_MISMATCH;

	req->source = dev;
	req->source_start = lba;
	req->read_req = block_alloc_request(dev);

	if (!req->read_req)
		return -BLOCK_COPY_NOMEM;

	req->read_req->req_done = block_copy_read_done;
	req->read_req->context = req;
	return 0;
}

/**
 * \brief Set target device and blocks for the block copy
 *
 * This function will set the target device and the address of the first
 * block to copy to in the block copy request descriptor.
 *
 * \param req     Block copy request descriptor
 * \param dev     Target device
 * \param lba     Address of first target block
 * \return        0 on success, negative error code on failure
 */
int block_copy_set_dest(
		struct block_copy_request *req,
		struct block_device *dev,
		block_addr_t lba)
{
	assert(req);
	assert(dev);

	if (req->block_size != dev->block_size)
		return -BLOCK_COPY_BLOCK_SIZE_MISMATCH;

	req->target = dev;
	req->target_start = lba;
	req->write_req = block_alloc_request(dev);

	if (!req->write_req)
		return -BLOCK_COPY_NOMEM;

	req->write_req->req_done = block_copy_write_done;
	req->write_req->context = req;
	return 0;
}

/**
 * \brief Set completion callback
 *
 * This function will set the callback called when the block copy request
 * completes. The callback will also be called if the block copy fails for
 * some reason; an error code can then be found in the status field.
 *
 * \param req       Block copy request descriptor
 * \param callback  Completion callback
 */
void block_copy_set_completion_callback(
		struct block_copy_request *req,
		void (*callback)
			(struct block_copy_request *, uint32_t, void *))
{
	assert(req);
	req->completion_callback = callback;
}

/**
 * \brief Set progress callback
 *
 * This function will set the callback called as the block copy request
 * progresses. The callback will be called after each successful write
 * operation, except for the last write in the request.
 *
 * \param req       Block copy request descriptor
 * \param callback  Progress callback
 */
void block_copy_set_progress_callback(
		struct block_copy_request *req,
		void (*callback)
			(struct block_copy_request *, uint32_t, void *))
{
	assert(req);
	req->progress_callback = callback;
}

/**
 * \brief Submit a block copy request
 *
 * This function will submit a block copy request and start the actual
 * copying. When the operation has completed the completion callback set by
 * block_copy_set_completion_callback() is executed.
 *
 * \param req       Block copy request descriptor
 */
void block_copy_submit(struct block_copy_request *req)
{
	assert(req);
	assert(req->source);
	assert(req->target);
	assert(req->read_req);
	assert(req->write_req);

	req->total_blocks_copied = 0;

	block_copy_read_blocks(req);
}
