/*
 * Copyright (c) 2004-2005 Endace Technology Ltd, Hamilton, New Zealand.
 * All rights reserved.
 *
 * This source code is proprietary to Endace Technology Limited and no part
 * of it may be redistributed, published or disclosed except as outlined in
 * the written contract supplied with this product.
 *
 * $Id: vdagapi.c 13301 2010-10-07 03:51:27Z alexey.korolev $
 */

/* C Standard Library headers. */
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>

#include <sys/ioctl.h>

/* Endace header */
#include "vdagapi.h"
#ifndef NDEBUG
#include "dagutil.h"
#endif

#define DAG_REG_BASE    0x900

typedef struct vdag_module_list* vdag_module_list_p;   
typedef struct vdag_module_list
{
	dag_reg_t module_node;
	struct vdag_module_list *next;
} vdag_module_list_t;

/****************************************************************************
 * FUNCTION: cleanup_module_list
 * DESCRIPTION: This is an internal functon that clean up the module linked list. 
 * PARAMETER: head - a pointer to the list of module
 ****************************************************************************/
static void
cleanup_module_list(vdag_module_list_p head)
{
	vdag_module_list_p temp = NULL;
	vdag_module_list_p next_module = NULL;

	temp = head;

	while(temp != NULL)
	{
		next_module = temp->next;
		free(temp);
		temp = next_module;
	}
}

/****************************************************************************
 * FUNCTION: read_module_list
 * DESCRIPTION: This is an internal function that read module list from DRB 
 * 		space and then contruct a linked list to store each of modules.
 * PARAMETER: head - a pointer to the list of module
 * 	      table - a pointer to an array of module
 * RETURN: -1 if failed, 0 if success
 ****************************************************************************/
static int
read_module_list(vdag_module_list_p *head, dag_reg_t *table)
{
	dag_reg_t* rptr = table;
	vdag_module_list_p tail = NULL;
	vdag_module_list_p node = NULL;

	if ((rptr->module != DAG_REG_START) || (rptr->addr != DAG_REG_ADDR_START))
	{
#ifndef	NDEBUG
		dagutil_error("[read_module_list] invalid register start\n");
#endif
		return -1;
	}
	
	/* Read drb enumeration table and construct the linked list */
	do
	{
		node = (vdag_module_list_t*) malloc(sizeof(vdag_module_list_t));
		memset(node,0, sizeof(vdag_module_list_t));
		memcpy(&node->module_node, rptr, sizeof(dag_reg_t));

		/* append modules into list */
		if (*head == NULL)
			*head  = node;
		else
			 tail->next = node;

		tail = node;

	} while (!((rptr++)->module == DAG_REG_END));	

	return 0;
}

/****************************************************************************
 * FUNCTION: write_module_list
 * DESCRIPTION: This is an internal function that write module based linked list 
 * 	     back to DRB space.
 * PARAMETER: head - a pointer to the list of module
 * 	      table - a pointer to an array of module
 * RETURN: -1 if failed, 0 if success
 ****************************************************************************/
static int
write_module_list(vdag_module_list_p head, dag_reg_t *table)
{
       dag_reg_t *rptr = table;
       vdag_module_list_p temp = NULL;
       temp = head;

       if (temp == NULL)
       {
#ifndef	NDEBUG
		dagutil_error("[write_module_list] head is empty\n");
#endif   
		return -1;
       }
			
       while ( temp != NULL )
       {
		*rptr = temp->module_node;
		rptr ++;
		temp = temp->next;
       }      
	
       return 0;
}

/****************************************************************************
 * FUNCTION: insert_module
 * DESCRIPTION: This is an internal function that add a firmware module into 
 * 		linked list by given name, address and version of 
 * 		module.
 * PARAMETER: head - a pointer to the list of module
 * 	      new_module - a new module to be inserted to linked list
 * RETURN: -1 if failed, 0 if suceess
 ****************************************************************************/
static int
insert_module(vdag_module_list_p head, dag_reg_t new_module)
{
	vdag_module_list_p temp = NULL;
	vdag_module_list_p tail = NULL;
	vdag_module_list_p new_node = NULL;

	new_node = (vdag_module_list_t*) malloc(sizeof(vdag_module_list_t));
	memcpy(&new_node->module_node, &new_module, sizeof(dag_reg_t));

	temp = head;
	if (temp == NULL)
	{
#ifndef	NDEBUG
		dagutil_error("[insert_module] List is empty\n");
#endif
		return -1;
	}

	tail = temp->next;
	while (tail != NULL )
	{	
		
		/* appended module in middle of linked list if two of name of module are same */
		if(temp->module_node.module == new_node->module_node.module)
		{		  
			if(temp->module_node.addr == new_node->module_node.addr)
			{
#ifndef NDEBUG
				dagutil_error("[insert_module] address conflict\n");
#endif
				return -1;
			}
	
			new_node->next = temp->next;
			temp->next = new_node;

			break;
		}

		/* appended module into the end of linked list */
		if(tail->module_node.module == DAG_REG_END)
		{
			temp->next = new_node;
			new_node->next = tail;
			break;
		}

		tail = tail->next;
		temp = temp->next;

	}

	return 0;

}
/****************************************************************************
 * FUNCTION: remove_node
 * DESCRIPTION: This is an internal function that is remove a module from 
 * 		linked list by given name of module and address.
 * PARAMETERS: head - a pointer to the list of module
 * 	       remove_module - a module need to be removed
 * RETURN: -1 if failed, 0 if success
 ****************************************************************************/
static int
remove_node(vdag_module_list_p head, dag_reg_t remove_module)
{
	vdag_module_list_p temp = NULL;
	vdag_module_list_p next_module = NULL;

	temp = head;

	if (temp == NULL)
	{
#ifndef	NDEBUG
		dagutil_error("[remove_node] List is empty\n");
#endif
		return -1;
	}
	next_module = temp->next;
	while (next_module != NULL)
	{
		/* free a node from linked list by given name and address of module*/ 
		if ( (next_module->module_node.module == remove_module.module) &&
		     (next_module->module_node.addr == remove_module.addr) )
		{
	
			temp->next = next_module->next;
			free(next_module);
			break;
		}

		temp = temp->next;
		next_module = next_module->next;
		
	}

	return 0;
}

/*****************************************************************************/
int vdag_set_device_info(int vdagfd, uint16_t dev_id, uint8_t brd_rev)
{
	uint32_t info;

	/* check the device id is invalid */
	if ((dev_id & 0xFFFF) == 0 )
	{
#ifndef	NDEBUG
		dagutil_error("[vdag_set_device_info] invalid device id\n");
#endif
		errno = EINVAL;
		return -1;
	}
	
	info = (brd_rev & 0xF) << 16 | dev_id;
	
	/* ioctl call to set the device id to driver. */
	if(ioctl(vdagfd, DAGIOCDEVINFO, &info) < 0) 
	{
#ifndef NDEBUG
		dagutil_error("[vdag_set_device_id] ioctl DAGIOCDEVINFO: fail%s\n", strerror(errno));
#endif
		return -1;
	}
	
	return 0;	
	
}

/*****************************************************************************/
int 
vdag_insert_module(int vdagfd, uint32_t reg_module, uint32_t addr, uint32_t ver)
{
	uint8_t *iom;
	dag_reg_t *rptr;
	dag_reg_t new_module; /* new module to be insert */
	vdag_module_list_p list = NULL;

	new_module.addr = addr & 0xFFFF;
	new_module.module = reg_module & 0xFF;
	new_module.version = ver & 0xF;
	new_module.flags = 0;

	if ((new_module.module == DAG_REG_START) || (new_module.addr == DAG_REG_ADDR_START) ||
	    (new_module.module == DAG_REG_END) || (new_module.addr == DAG_REG_ADDR_END))
	{
#ifndef	NDEBUG
		dagutil_error("[vdag_insert_module] Invalid module");
#endif
		errno = EINVAL;
		return -1;
	}

	iom = dag_iom(vdagfd);
       	if (iom == NULL)
	{
		errno = EBADF;
		return -1;
	}

	rptr = (dag_reg_t*)(iom+DAG_REG_BASE);
	/* read DRB space and construct it in linked list */
	if (read_module_list(&list, rptr) < 0)
	{
#ifndef	NDEBUG
		dagutil_error("[vdag_insert_module] read drb space failed\n");
#endif  
		errno = EIO;
		return -1;
	}
	
	/* remove a module from linked list with specific module name and address */
	if (insert_module(list, new_module) < 0)
	{
#ifndef	NDEBUG
		dagutil_error("[vdag_insert_module] insert node failed\n");
#endif
		return -1;
	}

	/* write linked list back to DRB space */
	if (write_module_list(list, rptr) < 0)
	{
#ifndef	NDEBUG
		dagutil_error("[vdag_insert_module] write drb space failed\n");
#endif
		errno = EIO;
		return -1;

	}

	cleanup_module_list(list);

	return 0;

}

/*****************************************************************************/
int 
vdag_remove_module(int vdagfd, uint32_t reg_module, uint32_t addr)
{
	uint8_t *iom;
	dag_reg_t *rptr;
	dag_reg_t remove_module; /* module to be remove */
	vdag_module_list_p list = NULL;
	 
	remove_module.addr = addr & 0xFFFF;
	remove_module.module = reg_module & 0xFF;
	remove_module.flags = 0;
	remove_module.version = 0;
	
	if ((remove_module.module == DAG_REG_START) || (remove_module.addr == DAG_REG_ADDR_START) ||
	    (remove_module.module == DAG_REG_END) || (remove_module.addr == DAG_REG_ADDR_END))
	{
#ifndef	NDEBUG
		dagutil_error( "[vdag_remove_module] can't remove this module");
#endif
		errno = EINVAL;
		return -1;
	}
	
	iom = dag_iom(vdagfd);
	if (iom == NULL)
	{
		errno = EBADF;
		return -1;
	}
	
	rptr = (dag_reg_t*)(iom+DAG_REG_BASE);
	/* read DRB space and construct it in linked list */
	if (read_module_list(&list, rptr) < 0)
	{
#ifndef	NDEBUG
		dagutil_error("[vdag_remove_module] read drb space failed\n");
#endif
		errno = EIO;
		return -1;
	}

	/* remove a module from linked list with specific module name and address */
	if (remove_node(list, remove_module) < 0)
	{
#ifndef	NDEBUG
		dagutil_error("vdag_remove_module] remove node failed\n");
#endif
		return -1;
	}
	
	/* write linked list back to DRB space */
	if (write_module_list(list, rptr) < 0)
	{
#ifndef	NDEBUG
		dagutil_error("vdag_remove_module] write drb space failed\n");
#endif
		errno = EIO;
		return -1;	
	}
	
	cleanup_module_list(list);

	return 0;
}

/*****************************************************************************/
int 
vdag_set_stream_count(int vdagfd, uint8_t rx_stream_num, uint8_t tx_stream_num)
{
	int total_stream_num;
	uint8_t *iom;
	dag_reg_t *rptr;
	vdag_module_list_p list = NULL;
	int prev_value;
	
	iom = dag_iom(vdagfd);
	if (iom == NULL)
	{
		errno = EBADF;
		return -1;
	}

	if (vdag_lock_all_stream(vdagfd))
	{
		errno = EACCES;
		goto exit;
	}

	rptr = (dag_reg_t*)(iom+DAG_REG_BASE);
	/* read DRB space and construct it in linked list */
	if (read_module_list(&list, rptr) < 0)
	{
#ifndef	NDEBUG
		dagutil_error("[vdag_set_stream_count] read drb space failed\n");
#endif
		errno = EIO;
		return -1;
	}
	
	/* 
 	 * find the PBM module and set the total number of streams into the 
 	 * address of stream count
 	 */	
	while (list != NULL)
	{
		if (list->module_node.module == DAG_REG_PBM)
		{
			switch (list->module_node.version)
			{
				case 0: 
					break;

				case 1:
				case 2: // version 2 CSBM
					total_stream_num = ((rx_stream_num & 0xf) << 20) | ((tx_stream_num & 0xf) << 24);
					prev_value = *(uint32_t*)(iom + list->module_node.addr);
					*(uint32_t*)(iom + list->module_node.addr) = total_stream_num | prev_value; 
					
					break;

				case 3: // version 3 HSBM
					total_stream_num = (rx_stream_num & 0xfff) | ((tx_stream_num & 0xfff) << 16);
					*(uint32_t*)(iom + list->module_node.addr + 0x0C) = total_stream_num; 	
					break;

				default:
					errno = EIO;
					return -1;
			}
		} else if (list->module_node.module == DAG_REG_STREAM_FTR) {
			switch (list->module_node.version) {
			case 0: 
				*(uint32_t*)(iom + list->module_node.addr) = 0x04000000 + rx_stream_num;
				break;
			default:
				errno = EIO;
				return -1;
			}
		}
		

		list = list->next;
	}

	cleanup_module_list(list);

	/* read DRB space and construct it in linked list */
	if (read_module_list(&list, rptr) < 0)
	{
#ifndef	NDEBUG
		dagutil_error("[vdag_set_stream_count] read drb space failed\n");
#endif
		errno = EIO;
		return -1;
	}
	
	/* 
 	 * find the Stream Feature module and set the total number of streams
 	 */	
	while (list != NULL)
	{

		list = list->next;
	}

	cleanup_module_list(list);

exit:
	vdag_unlock_all_stream(vdagfd);
	
	return 0;

}

/*****************************************************************************/
int 
vdag_lock_all_stream(int vdagfd)
{
	int lock;
	
	lock = (DAG_LOCK_CARD | DAG_LOCK_OP_ACQUIRE);

	if(ioctl(vdagfd, DAGIOCLOCK, &lock) < 0)
	{
		return -1;
	}

	return 0;
}

/*****************************************************************************/
int 
vdag_unlock_all_stream(int vdagfd)
{
	int unlock;
	
	unlock = (DAG_LOCK_CARD | DAG_LOCK_OP_RELEASE);

	if(ioctl(vdagfd, DAGIOCLOCK, &unlock) < 0)
	{
		return -1;
	}

	return 0;
}

/*****************************************************************************/
int 
vdag_alloc_memory(int vdagfd, uint32_t size, int32_t node)
{
	user_mem_t mem_user;

	mem_user.node = node;
	mem_user.membuf_size=size;

	if (vdag_lock_all_stream(vdagfd))
	{
		errno = EACCES;
		return -1;
	}

	/* free memory */
	if (ioctl(vdagfd, DAGIOCFREEMEM) < 0)
	{
		errno = EFAULT;
		goto exit;
	}

	/* allocate memory */
	if (ioctl(vdagfd, DAGIOCALLOCMEM, &mem_user) < 0)
	{
		errno = ENOMEM;
		goto exit;
	}
	vdag_unlock_all_stream(vdagfd);
	return 0;

exit:
	vdag_unlock_all_stream(vdagfd);
	return -1;

}
