/* ----------------------------------------------------------------------------
 *         SAM Software Package License
 * ----------------------------------------------------------------------------
 * Copyright (c) 2015, Atmel Corporation
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the disclaimer below.
 *
 * Atmel's name may not be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * DISCLAIMER: 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
 * 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.
 * ----------------------------------------------------------------------------
 */


#ifdef CONFIG_HAVE_FLEXCOM
#include "peripherals/flexcom.h"
#endif
#include "peripherals/pmc.h"
#include "peripherals/twid.h"
#include "peripherals/twi.h"
#include "peripherals/xdmad.h"
#include "peripherals/l2cc.h"

#include "cortex-a/cp15.h"

#include "trace.h"
#include "io.h"
#include "timer.h"

#include <assert.h>
#include <string.h>

#define TWID_DMA_THRESHOLD      16
#define TWID_TIMEOUT            100

static uint32_t _twid_wait_twi_transfer(struct _twi_desc* desc)
{
	struct _timeout timeout;
	timer_start_timeout(&timeout, TWID_TIMEOUT);
	while(!twi_is_transfer_complete(desc->addr)){
		if (timer_timeout_reached(&timeout)) {
			trace_error("twid: Unable to complete transfert!\r\n");
			twid_configure(desc);
			return TWID_ERROR_TRANSFER;
		}
	}
	return TWID_SUCCESS;
}

static void _twid_xdmad_callback_wrapper(struct _xdmad_channel* channel,
					   void* args)
{
	trace_debug("TWID DMA Transfert Finished\r\n");
	struct _twi_desc* twid = (struct _twi_desc*) args;

	xdmad_free_channel(channel);

	if (twid->region_start && twid->region_end) {
		l2cc_invalidate_region(twid->region_start, twid->region_end);
	}

	if (twid && twid->callback)
		twid->callback(twid, twid->cb_args);

}

static void _twid_init_dma_read_channel(const struct _twi_desc* desc,
					  struct _xdmad_channel** channel,
					  struct _xdmad_cfg* cfg)
{
	assert(cfg);
	assert(channel);

	uint32_t id = get_twi_id_from_addr(desc->addr);
	assert(id < ID_PERIPH_COUNT);

	memset(cfg, 0x0, sizeof(*cfg));

	*channel =
		xdmad_allocate_channel(id, XDMAD_PERIPH_MEMORY);
	assert(*channel);

	xdmad_prepare_channel(*channel);
	cfg->cfg.uint32_value = XDMAC_CC_TYPE_PER_TRAN
		| XDMAC_CC_DSYNC_PER2MEM
		| XDMAC_CC_MEMSET_NORMAL_MODE
		| XDMAC_CC_CSIZE_CHK_1
		| XDMAC_CC_DWIDTH_BYTE
		| XDMAC_CC_DIF_AHB_IF0
		| XDMAC_CC_SIF_AHB_IF1
		| XDMAC_CC_SAM_FIXED_AM;

	cfg->src_addr = (void*)&desc->addr->TWI_RHR;
}

static void _twid_dma_read(const struct _twi_desc* desc,
			   struct _buffer* buffer)
{
	struct _xdmad_channel* channel = NULL;
	struct _xdmad_cfg cfg;

	_twid_init_dma_read_channel(desc, &channel, &cfg);

	cfg.cfg.bitfield.dam = XDMAC_CC_DAM_INCREMENTED_AM
		>> XDMAC_CC_DAM_Pos;
	cfg.dest_addr = buffer->data;
	cfg.ublock_size = buffer->size;
	cfg.block_size = 0;
	xdmad_configure_transfer(channel, &cfg, 0, 0);
	xdmad_set_callback(channel, _twid_xdmad_callback_wrapper,
			   (void*)desc);

	l2cc_clean_region(desc->region_start, desc->region_end);

	xdmad_start_transfer(channel);
}

static void _twid_init_dma_write_channel(struct _twi_desc* desc,
					 struct _xdmad_channel** channel,
					 struct _xdmad_cfg* cfg)
{
	assert(cfg);
	assert(channel);

	uint32_t id = get_twi_id_from_addr(desc->addr);
	assert(id < ID_PERIPH_COUNT);
	memset(cfg, 0x0, sizeof(*cfg));

	*channel =
		xdmad_allocate_channel(XDMAD_PERIPH_MEMORY, id);
	assert(*channel);

	xdmad_prepare_channel(*channel);
	cfg->cfg.uint32_value = XDMAC_CC_TYPE_PER_TRAN
		| XDMAC_CC_DSYNC_MEM2PER
		| XDMAC_CC_MEMSET_NORMAL_MODE
		| XDMAC_CC_CSIZE_CHK_1
		| XDMAC_CC_DWIDTH_BYTE
		| XDMAC_CC_DIF_AHB_IF1
		| XDMAC_CC_SIF_AHB_IF0
		| XDMAC_CC_DAM_FIXED_AM;

	cfg->dest_addr = (void*)&desc->addr->TWI_THR;
}

static void _twid_dma_write(struct _twi_desc* desc,
			    struct _buffer* buffer)
{
	struct _xdmad_channel* channel = NULL;
	struct _xdmad_cfg cfg;

	_twid_init_dma_write_channel(desc, &channel, &cfg);

	cfg.cfg.bitfield.sam = XDMAC_CC_SAM_INCREMENTED_AM
		>> XDMAC_CC_SAM_Pos;
	cfg.src_addr = buffer->data;
	cfg.ublock_size = buffer->size;
	cfg.block_size = 0;
	xdmad_configure_transfer(channel, &cfg, 0, 0);
	xdmad_set_callback(channel, _twid_xdmad_callback_wrapper,
			   (void*)desc);

	l2cc_clean_region(desc->region_start, desc->region_end);

	xdmad_start_transfer(channel);
}

void twid_configure(struct _twi_desc* desc)
{
	uint32_t id = get_twi_id_from_addr(desc->addr);
	assert(id < ID_PERIPH_COUNT);

#ifdef CONFIG_HAVE_FLEXCOM
	Flexcom* flexcom = get_flexcom_addr_from_id(get_twi_id_from_addr(desc->addr));
	if (flexcom) {
		flexcom_select(flexcom, FLEX_MR_OPMODE_TWI);
	}
#endif

	pmc_enable_peripheral(id);
	twi_configure_master(desc->addr, desc->freq);

#ifdef CONFIG_HAVE_TWI_FIFO
	if (desc->transfert_mode == TWID_MODE_FIFO) {
		uint32_t fifo_depth = get_peripheral_fifo_depth(desc->addr);
		twi_fifo_configure(desc->addr, fifo_depth/2, fifo_depth/2,
				   TWI_FMR_RXRDYM_ONE_DATA | TWI_FMR_TXRDYM_ONE_DATA);
	}
#endif
}

static uint32_t _twid_poll_write(struct _twi_desc* desc, struct _buffer* buffer)
{
	int i = 0;
	struct _timeout timeout;
	twi_init_write_transfert(desc->addr,
				 desc->slave_addr,
				 desc->iaddr,
				 desc->isize,
				 buffer->size);
	if (twi_get_status(desc->addr) & TWI_SR_NACK) {
		trace_error("twid: command NACK!\r\n");
		return TWID_ERROR_ACK;
	}
	for (i = 0; i < buffer->size; ++i) {
		timer_start_timeout(&timeout, TWID_TIMEOUT);
		while(!twi_byte_sent(desc->addr)) {
			if (timer_timeout_reached(&timeout)) {
				trace_error("twid: Device doesn't answer, "
					    "(TX TIMEOUT)\r\n");
				break;
			}
		}
		twi_write_byte(desc->addr, buffer->data[i]);
		if(twi_get_status(desc->addr) & TWI_SR_NACK) {
			trace_error("twid: command NACK!\r\n");
			return TWID_ERROR_ACK;
		}
	}
	/* wait transfert to be finished */
	return _twid_wait_twi_transfer(desc);
}

static uint32_t _twid_poll_read(struct _twi_desc* desc, struct _buffer* buffer)
{
	int i = 0;
	struct _timeout timeout;
	twi_init_read_transfert(desc->addr,
				desc->slave_addr,
				desc->iaddr,
				desc->isize,
				buffer->size);
	if (twi_get_status(desc->addr) & TWI_SR_NACK) {
		trace_error("twid: command NACK!\r\n");
		return TWID_ERROR_ACK;
	}
	for (i = 0; i < buffer->size; ++i) {
		timer_start_timeout(&timeout, TWID_TIMEOUT);
		while(!twi_is_byte_received(desc->addr)) {
			if (timer_timeout_reached(&timeout)) {
				trace_error("twid: Device doesn't answer, "
					    "(RX TIMEOUT)\r\n");
				break;
			}
		}
		buffer->data[i] = twi_read_byte(desc->addr);
		if(twi_get_status(desc->addr) & TWI_SR_NACK) {
			trace_error("twid: command NACK\r\n");
		return TWID_ERROR_ACK;
		}
	}
	/* wait transfert to be finished */
	return _twid_wait_twi_transfer(desc);
}

uint32_t twid_transfert(struct _twi_desc* desc, struct _buffer* rx,
			struct _buffer* tx, twid_callback_t cb,
			void* user_args)
{
	uint32_t status = TWID_SUCCESS;

	desc->callback = cb;
	desc->cb_args = user_args;

	if (mutex_try_lock(&desc->mutex)) {
		return TWID_ERROR_LOCK;
	}

	switch (desc->transfert_mode) {
	case TWID_MODE_POLLING:
		if (tx) {
			status = _twid_poll_write(desc, tx);
			if (status) break;
		}
		if (rx) {
			status = _twid_poll_read(desc, rx);
			if (status) break;
		}
		if (cb)
			cb(desc, user_args);
		mutex_free(&desc->mutex);
		break;

	case TWID_MODE_DMA:
		if (!(rx || tx)) {
			status = TWID_ERROR_DUPLEX;
			break;
		}
		if (tx) {
			if (tx->size < TWID_DMA_THRESHOLD) {
				status = _twid_poll_write(desc, tx);
				if (status) break;
				if (cb)
					cb(desc, user_args);
				mutex_free(&desc->mutex);
			} else {
				twi_init_write_transfert(desc->addr,
							 desc->slave_addr,
							 desc->iaddr,
							 desc->isize,
							 tx->size);
				desc->region_start = (uint32_t)tx->data;
				desc->region_end = desc->region_start
					+ tx->size;
				_twid_dma_write(desc, tx);
			}
		}
		if (rx) {
			if (rx->size < TWID_DMA_THRESHOLD) {
				status = _twid_poll_read(desc, rx);
				if (status) break;
				if (cb)
					cb(desc, user_args);
				mutex_free(&desc->mutex);
			} else {
				twi_init_read_transfert(desc->addr,
							desc->slave_addr,
							desc->iaddr,
							desc->isize,
							rx->size);
				desc->region_start = (uint32_t)rx->data;
				desc->region_end = desc->region_start
					+ rx->size;
				if(twi_get_status(desc->addr) & TWI_SR_NACK) {
					trace_error("twid: Acknolegment "
						    "Error\r\n");
					status = TWID_ERROR_ACK;
					break;
				}
				_twid_dma_read(desc, rx);
			}
		}
		break;

#ifdef CONFIG_HAVE_TWI_FIFO
	case TWID_MODE_FIFO:
		if (tx) {
			status = twi_write_stream(desc->addr, desc->slave_addr,
						  desc->iaddr, desc->isize,
						  tx->data, tx->size);
			status = status ? TWID_SUCCESS : TWID_ERROR_ACK;
			if (status)
				break;
			status = _twid_wait_twi_transfer(desc);
			if (status)
				break;
		}
		if (rx) {
			status = twi_read_stream(desc->addr, desc->slave_addr,
						 desc->iaddr, desc->isize,
						 rx->data, rx->size);
			status = status ? TWID_SUCCESS : TWID_ERROR_ACK;
			if (status)
				break;
			status = _twid_wait_twi_transfer(desc);
			if (status)
				break;
		}
		if (cb)
			cb(desc, user_args);
		mutex_free(&desc->mutex);
		break;
#endif
	default:
		trace_debug("Unkown mode");
	}

	if (status)
		mutex_free(&desc->mutex);

	return status;
}

void twid_finish_transfert_callback(struct _twi_desc* desc, void* user_args)
{
	(void)user_args;
	twid_finish_transfert(desc);
}

void twid_finish_transfert(struct _twi_desc* desc)
{
	mutex_free(&desc->mutex);
}

uint32_t twid_is_busy(const struct _twi_desc* desc)
{
	return mutex_is_locked(&desc->mutex);
}

void twid_wait_transfert(const struct _twi_desc* desc)
{
	while (mutex_is_locked(&desc->mutex));
}
