/**
* This is a general purpose adapter between SICS and EPICS. It provides callbacks 
* which allows Hipadaba nodes to be connected to EPICS PV's for both reading and 
* writing. 
*
* copyright: see file COPYRIGHT
*
* Mark Koennecke, October 2014
*/
#include <assert.h>
#include <sics.h>
#include <sicshipadaba.h>
#include <cadef.h>
#include <epicsMessageQueue.h>
#include <epicsThread.h>
#include <messagepipe.h>

/*
One of those: alarmString.h cannot be doubly included into the 
same application
*/
/*#include <alarmString.h> */
extern char *epicsAlarmConditionStrings[];

/*
* we have a SICS tasks which polls EPICS regularly.
*/
static long epicsTaskID = -1L;
static pMP readPipe = NULL;
static epicsMessageQueueId writeQueue;


typedef struct {
	char pvname[64];
	int connected;
	pHdb node;
	chid pvchid;
	chtype pvtype;
}EpicsPriv, *pEpicsPriv;

typedef struct {
	SConnection *pCon;
	char message[512];
} WriteMessage, *pWriteMessage;

/*======================================================================================
  Code for reading PV's
  ======================================================================================*/
static int EpicsTask(void *userData)
{
	WriteMessage mes;

	/*
		drive the main EPICS loop for subscriptions
	*/ 
	ca_poll();

	/*
		process possible messages from the writing threads
	*/
	if(epicsMessageQueueTryReceive(writeQueue,&mes,sizeof(mes)) > 0){
		if(mes.pCon == NULL){
			traceIO("epics",mes.message);
		} else {
			SCWrite(mes.pCon,mes.message,eError);
			SCDeleteConnection(mes.pCon);
		}
	}
	return 1;
}  
/*---------------------------------------------------------------------------------------*/
static int makeEPICSContext(void *message, void *userData)
{
	int status;
	pEpicsPriv priv = NULL;

	if(epicsTaskID < 0){
		status = ca_context_create(ca_disable_preemptive_callback);
		if(status != ECA_NORMAL){
			priv = (pEpicsPriv)message;
			SetHdbProperty(priv->node,"geterror", "Failed to create EPICS context");
			return MPSTOP;
		}
		epicsTaskID = TaskRegisterN(pServ->pTasker,
			"epics",
			EpicsTask,
			NULL,NULL,NULL,1
			);
	}
	return MPCONTINUE;
}  
/*--------------------------------------------------------------------------------------*/
static int epicsConnectPV(void *message, void *userData)
{
	pEpicsPriv priv = NULL;
	int status;

	priv = (pEpicsPriv)message;

	status = ca_create_channel(priv->pvname,NULL,NULL,10,&priv->pvchid);
	if(status != ECA_NORMAL){
		SetHdbProperty(priv->node,"geterror", "Failed to connect to PV");
		return MPSTOP;
	}
	status = ca_pend_io(3.);
	if(status != ECA_NORMAL){
		SetHdbProperty(priv->node,"geterror", "Timeout connecting to PV");
		return MPSTOP;
	}
	return MPCONTINUE;
}
/*--------------------------------------------------------------------------------------*/
static void epicsDataCallback(struct event_handler_args args)
{
	pEpicsPriv priv = NULL;
	hdbValue v;
	char error[256];

	priv = (pEpicsPriv)args.usr;

	if(args.status == ECA_NORMAL){
		switch(priv->node->value.dataType){
			case HIPTEXT:
				free(priv->node->value.v.text);
				priv->node->value.v.text = strdup((char *)args.dbr);
				break;
			case HIPINT:
				priv->node->value.v.intValue = *(int *)args.dbr;
				break;
			case HIPFLOAT:
				priv->node->value.v.doubleValue = *(double *)args.dbr;
				break;
			case HIPINTAR:
			case HIPINTVARAR:
				v = MakeHdbIntArray(args.count,(int *)args.dbr);
				copyHdbValue(&v,&priv->node->value);
				break;
			case HIPFLOATAR:
			case HIPFLOATVARAR:
				v = MakeHdbFloatArray(args.count,(double *)args.dbr);
				copyHdbValue(&v,&priv->node->value);
				break;
		}
		SetHdbProperty(priv->node,"geterror",NULL);
		traceIO("epics","Received data for %s", priv->node->name);
		NotifyHipadabaPar(priv->node,NULL);
	} else {
		snprintf(error,sizeof(error),"ERROR: %s for node %s", ca_message(args.status), priv->node->name);
		traceIO("epics",error);
		SetHdbProperty(priv->node,"geterror",error);	
	}
}
/*--------------------------------------------------------------------------------------*/
static int epicsSubscribePV(void *message, void *userData)
{
	pEpicsPriv priv = NULL;
	int status;
	chtype subType = DBR_STRING;
	evid eid;

	priv = (pEpicsPriv)message;
	switch(priv->node->value.dataType){
		case HIPTEXT:
			subType = DBR_STRING;
			break;
		case HIPINT:
		case HIPINTAR:
		case HIPINTVARAR:
			subType = DBR_LONG;
			break;
		case HIPFLOAT:
		case HIPFLOATAR:
		case HIPFLOATVARAR:
			subType = DBR_DOUBLE;
			break;
	}

	status = ca_create_subscription(subType,0,priv->pvchid,
		DBE_VALUE|DBE_ALARM,epicsDataCallback,priv,&eid);
	if(status != ECA_NORMAL){
		SetHdbProperty(priv->node,"geterror", "Failed to subscribe to PV");
		return MPSTOP;
	}


	return MPCONTINUE;
}
/*--------------------------------------------------------------------------------------*/  
static void createEPICSReadPipe()
{
	readPipe = MakeMP();
	AppendMPFilter(readPipe,makeEPICSContext,NULL,NULL);
	AppendMPFilter(readPipe,epicsConnectPV,NULL,NULL);
	AppendMPFilter(readPipe,epicsSubscribePV,NULL,NULL);
}
/*--------------------------------------------------------------------------------------*/
static void connectPV(pHdb node, pEpicsPriv priv)
{
	int status;

	SetHdbProperty(node,"geterror", NULL);
	priv->node = node;
	status = MPprocess(readPipe, priv);
	if(status == MPCONTINUE){
		priv->connected = 1;
	}
}
/*--------------------------------------------------------------------------------------
  This is the Hipadaba callback function
  --------------------------------------------------------------------------------------*/
		static hdbCallbackReturn EPICSReadCallback(pHdb currentNode, 
					void *userData,
					pHdbMessage message)
		{
				pEpicsPriv priv = (pEpicsPriv)userData;
			    hdbDataMessage *mm = NULL;
			    SConnection *con = NULL;
			    char *geterror;
			    char error[256];
                enum channel_state cs;

				assert(priv != NULL);

				mm = GetHdbGetMessage(message);
			  if (mm != NULL) {
			    con = mm->callData;

			    if(priv->connected != 1){
			    	connectPV(currentNode, priv);
			    }
                cs = ca_state(priv->pvchid);
                if(cs != cs_conn){
                	SCWrite(con,"ERROR: epics disconnected", eError);
                	return hdbAbort;
                }
			    geterror = GetHdbProp(currentNode, "geterror");
			    if (geterror != NULL) {
			      snprintf(error,sizeof(error),"ERROR: %s", geterror); 
			      SCWrite(con, error, eError);
			      if (mm->v->dataType == HIPTEXT) {
			        if (mm->v->v.text != NULL) {
			          free(mm->v->v.text);
			        }
			        mm->v->v.text = strdup(error);
			      }
			      return hdbAbort;
			    }
			    return hdbContinue;
			  }

			  return hdbContinue;
		}
	/*------------------------------------------------------------------------------------*/
	static int EpicsConnectRead(pSICSOBJ ccmd, SConnection * con,
	                      Hdb * cmdNode, Hdb * par[], int nPar)
	{
		pHdb node = NULL;
		pEpicsPriv priv = NULL;

		if(nPar < 2){
			SCWrite(con,"ERROR: need node and PV-name arguments to connectread", eError);
			return 0;
		}

		node = FindHdbNode(NULL,par[0]->value.v.text,con);
		if(node == NULL){
			SCPrintf(con,eError,"ERROR: failed to locate node %s", par[0]->value.v.text);
			return 0;
		}

		priv = calloc(1,sizeof(EpicsPriv));
		priv->node = node;
		strncpy(priv->pvname,par[1]->value.v.text,sizeof(priv->pvname));
		SetHdbProperty(node,"readpv", par[1]->value.v.text);
		AppendHipadabaCallback(node,MakeHipadabaCallback(EPICSReadCallback, priv,free));

		SCSendOK(con);

		return 1;
	}
/*==============================================================================================
	Writing Things. Writing can block, thus it has to run in its own thread. This raises the 
	question how to propagate error messages. The solution is a EPICS message queue to which 
	writing threads post. The epics task will read this queue and do the actual printing in the 
	SICS main thread. A convention: NULL means to print to trace.
================================================================================================*/
typedef struct {
	SConnection *pCon;
	hdbValue v;
	char pvName[64];
} WritePar, *pWritePar;	
/*----------------------------------------------------------------------------------------------*/
static void writeEpicsMessage(void *target, char *txt)
{
	pWriteMessage wm = NULL;

	wm = calloc(1,sizeof(WriteMessage));
	if(wm != NULL){
		if(target != NULL){
			wm->pCon = SCCopyConnection(target);
		}
		strncpy(wm->message,txt,sizeof(wm->message));
		epicsMessageQueueSend(writeQueue,wm,sizeof(WriteMessage));
	}
}
/*----------------------------------------------------------------------------------------------*/
static void epicsEndCallback(struct event_handler_args args)
{
	char message[512];
	snprintf(message,sizeof(message),"%s finished with %s", (char *)args.usr, 
		ca_message(args.status));
	writeEpicsMessage(NULL,message);
	free(args.usr);
}
/*----------------------------------------------------------------------------------------------*/
static void EpicsWriteFunc(void *param)
{
	pWritePar wp = (pWritePar)param;
	pWriteMessage wm = NULL;
	int status;
	chid cid;
	char error[512];
	char *pv;

	status = ca_context_create(ca_disable_preemptive_callback);
	if(status != ECA_NORMAL){
		writeEpicsMessage(wp->pCon,"ERROR: failed to create EPICS context for write");
		goto cleanup;
	}
	status = ca_create_channel(wp->pvName,NULL,NULL,10,&cid);
	if(status != ECA_NORMAL){
		snprintf(error,sizeof(error),"ERROR: failed to create EPICS channel for %s", wp->pvName);
		writeEpicsMessage(wp->pCon,error);
		goto cleanup;
	}
	status = ca_pend_io(5.);
	if(status != ECA_NORMAL){
		snprintf(error,sizeof(error),"ERROR: failed to connect EPICS channel for %s", wp->pvName);
		writeEpicsMessage(wp->pCon,error);
		goto cleanup;
	}

	pv = strdup(wp->pvName);
	switch(wp->v.dataType){
		case HIPINT:
			status = ca_put_callback(DBR_LONG,cid,&wp->v.v.intValue,epicsEndCallback,pv);
			break;
		case HIPFLOAT:
			status = ca_put_callback(DBR_DOUBLE,cid,&wp->v.v.doubleValue,epicsEndCallback,pv);
			break;
		case HIPTEXT:
			status = ca_put_callback(DBR_STRING,cid,wp->v.v.text,epicsEndCallback, pv);
			break;
		case HIPINTVARAR:
		case HIPINTAR:
			status = ca_array_put_callback(DBR_LONG,wp->v.arrayLength, 
					cid, wp->v.v.intArray,epicsEndCallback,pv);
			break;
		case HIPFLOATVARAR:
		case HIPFLOATAR:
			status = ca_array_put_callback(DBR_DOUBLE,wp->v.arrayLength, 
					cid, wp->v.v.floatArray,epicsEndCallback,pv);
			break;		
	}	
	if(status != ECA_NORMAL){
		snprintf(error,sizeof(error),"ERROR: failed to write to EPICS channel for %s with %d", wp->pvName, status);
		writeEpicsMessage(wp->pCon,error);
		goto cleanup;
	}
	writeEpicsMessage(wp->pCon,"OK");

	ca_pend_io(0);

	goto cleanup;
	cleanup:
		SCDeleteConnection(wp->pCon);
		ReleaseHdbValue(&wp->v);
		free(wp);
}
/*----------------------------------------------------------------------------------------------*/
static hdbCallbackReturn EPICSWriteCallback(pHdb currentNode, 
						void *userData,
						pHdbMessage message)
{
	hdbDataMessage *mm = NULL;
	pWritePar par = NULL;

	mm = GetHdbSetMessage(message);
	if(mm != NULL){
		par = calloc(1,sizeof(WritePar));
		if(par == NULL){
			SCWrite(mm->callData,"ERROR: out of memory in EPICSWriteCallback", eError);
			return hdbAbort;
		}
		par->pCon = SCCopyConnection(mm->callData);
		cloneHdbValue(mm->v,&par->v);
		strncpy(par->pvName,(char *)userData,sizeof(par->pvName));
		epicsThreadCreate("Write", 
		    epicsThreadPriorityHigh,
		    epicsThreadStackMedium,
		    EpicsWriteFunc,
		    par);
	}

	return hdbContinue;
}
/*------------------------------------------------------------------------------------*/
	static int EpicsConnectWrite(pSICSOBJ ccmd, SConnection * con,
	                      Hdb * cmdNode, Hdb * par[], int nPar)
	{
		pHdb node = NULL;
		pEpicsPriv priv = NULL;

		if(nPar < 2){
			SCWrite(con,"ERROR: need node and PV-name arguments to connectread", eError);
			return 0;
		}

		node = FindHdbNode(NULL,par[0]->value.v.text,con);
		if(node == NULL){
			SCPrintf(con,eError,"ERROR: failed to locate node %s", par[0]->value.v.text);
			return 0;
		}

		SetHdbProperty(node,"writepv", par[1]->value.v.text);
		RemoveSetUpdateCallback(node);
		AppendHipadabaCallback(node,MakeHipadabaCallback(EPICSWriteCallback, 
			strdup(par[1]->value.v.text),free));

		SCSendOK(con);

		return 1;
	}

/*==============================================================================================
	SICS Hydraulics
================================================================================================*/
	static int EpicsConvertAlarm(pSICSOBJ ccmd, SConnection * con,
	                      Hdb * cmdNode, Hdb * par[], int nPar)
	{
		pHdb node = NULL;
		char value[256];

		if(nPar < 1){
			SCWrite(con,"ERROR: need alarm code to convert", eError);
			return 0;
		}

		snprintf(value,sizeof(value),"%s", epicsAlarmConditionStrings[par[0]->value.v.intValue]);
		SCWrite(con,value,eValue);
		return 1;
	}


/*----------------------------------------------------------------------------------------------*/
int MakeEpicsAdapter(SConnection * con, SicsInterp * sics,
                             void *object, int argc, char *argv[])
{
	pSICSOBJ self = NULL;
	pHdb child, par;

	self = MakeSICSOBJv("epicsadapter", "EpicsAdapter", HIPNONE, usMugger);
	createEPICSReadPipe();
	writeQueue = epicsMessageQueueCreate(64,sizeof(WriteMessage));

	child = AddSICSHdbPar(self->objectNode,
                      "connectread", usMugger, MakeSICSFunc(EpicsConnectRead));
  	AddSICSHdbPar(child, "node", usMugger, MakeHdbText(""));
    AddSICSHdbPar(child, "pvname", usMugger, MakeHdbText(""));

	child = AddSICSHdbPar(self->objectNode,
                      "connectwrite", usMugger, MakeSICSFunc(EpicsConnectWrite));
  	AddSICSHdbPar(child, "node", usMugger, MakeHdbText(""));
    AddSICSHdbPar(child, "pvname", usMugger, MakeHdbText(""));

	child = AddSICSHdbPar(self->objectNode,
                      "convertalarm", usSpy, MakeSICSFunc(EpicsConvertAlarm));
  	AddSICSHdbPar(child, "stat", usSpy, MakeHdbInt(1.));

	AddCommand(pServ->pSics, "epicsadapter", InterInvokeSICSOBJ, KillSICSOBJ, self);
	return 1;
}
