# ca_util.py - a thin wrapper around CaChannel
# Tim Mooney 12/05/2008
#
# Modified by Xiaoqiang Wang to be Python 3 compatible.

"""ca_util.py is a wrapper around CaChannel that allows the caller to write,
e.g.,
    caget("xxx:m1")
instead of having to write
    m1 = CaChannel()
    m1.searchw("xxx:m1")
    m1.getw()
Also, ca_util defends against null PV names and some effects of short-term
CA disconnections, and it can verify that caput*() operations succeeded.
"""

version = "2.0"

import ca
import CaChannel
import time
import sys

# DBR types
# ca.DBR_STRING = 0
# ca.DBR_SHORT = 1 
# ca.DBR_INT = 1 
# ca.DBR_FLOAT = 2
# ca.DBR_ENUM = 3
# ca.DBR_CHAR = 4
# ca.DBR_LONG = 5
# ca.DBR_DOUBLE = 6

# If caller imported CaChannel using "from CaChannel import *", then the
# class CaChannel will have the same name as the module CaChannel, and
# we won't be able to see the module attribute, 'CaChannel.__file__'.
def getCaChannelFileName():
	""" For internal ca_util use"""
	return CaChannel.__file__

#######################################################################
# Human readable exception description
#	try:
#		x = x + 1
#	except:
#		print formatExceptionInfo()
import sys
import traceback
def formatExceptionInfo(maxTBlevel=5):
	cla, exc, trbk = sys.exc_info()
	excName = cla.__name__
	try:
		excArgs = exc.__dict__["args"]
	except KeyError:
		excArgs = "<no args>"
	excTb = traceback.format_tb(trbk, maxTBlevel)
	return (excName, excArgs, excTb)

#######################################################################
# channel-access connection states
ca_states = {}
# ...from cadef.h:
ca_states[ca.cs_never_conn] = "never connected"
ca_states[ca.cs_prev_conn] = "previously connected"
ca_states[ca.cs_conn] = "connected"
ca_states[ca.cs_closed] = "closed"
ca_states[ca.cs_never_search] = "never searched"



#######################################################################
# default settings for ca_util
defaultTimeout = None		# 'None' means use CaChannel's timeout
defaultRetries = 3
readCheckTolerance = None	# 'None" means don't check

def set_ca_util_defaults(timeout=None, retries=None, read_check_tolerance=None):
	"""
	usage: old = set_ca_util_defaults(timeout=None, retries=None,
	             read_check_tolerance=None)
	alternate: set_ca_util_defaults(defaultsList), where defaultsList is like
	the list returned by get_ca_util_defaults()
	Setting an argument to the string "NONE" disables it.
	Returns the list of previous default values:
	    [defaultTimeout, defaultRetries, readCheckTolerance]
	"""
	global defaultTimeout, defaultRetries, readCheckTolerance
	old = [defaultTimeout, defaultRetries, readCheckTolerance]
	if type(timeout) == type([]):
		argList = timeout
		timeout = argList[0]
		retries = argList[1]
		read_check_tolerance = argList[2]
	if (timeout!=None) : defaultTimeout = timeout
	if (retries!=None) : defaultRetries = retries
	if (read_check_tolerance!=None) : readCheckTolerance = read_check_tolerance
	return old

def get_ca_util_defaults():
	"""
	usage: myList = get_ca_util_defaults()
	myList is set to [defaultTimeout, defaultRetries, readCheckTolerance]
	"""
	global defaultTimeout, defaultRetries, readCheckTolerance
	return [defaultTimeout, defaultRetries, readCheckTolerance]

def set_ca_util_default_timeout(timeout=None):
	"""
	usage: old = set_ca_util_default_timeout(timeout=None)
	If timeout == "NONE", then ca_util doesn't specify any timeout in
	calls to underlying software.
	Returns previous default timeout.
	"""
	global defaultTimeout
	old = defaultTimeout
	defaultTimeout = timeout
	return old

def get_ca_util_default_timeout():
	global defaultTimeout
	return defaultTimeout

def set_ca_util_default_retries(retries=None):
	"""
	usage: old = set_ca_util_default_retries(retries=None)
	If retries == "NONE", then ca_util doesn't do any retries.
	Returns previous default retries.
	"""
	global defaultRetries
	old = defaultRetries
	defaultRetries = retries
	return old

def get_ca_util_default_retries():
	global defaultRetries
	return defaultRetries

def set_ca_util_default_read_check_tolerance(read_check_tolerance=None):
	"""
	usage: old = set_ca_util_default_read_check_tolerance(read_check_tolerance=None)
	If read_check_tolerance == "NONE", then ca_util doesn't compare the value
	it reads to the value it wrote.
	Returns previous default tolerance.
	"""
	global readCheckTolerance
	old = readCheckTolerance
	readCheckTolerance = read_check_tolerance
	return old

def get_ca_util_default_read_check_tolerance():
	global readCheckTolerance
	return readCheckTolerance


#######################################################################
# The dictionary, cadict, will be used to associate PV names with the
# machinery required to talk to EPICS PV's.  If no entry is found (the
# name hasn't been used yet in a ca call), then we create a new instance
# of CaChannel, connect it to the PV, and put it in the dictionary.  We also
# include a flag some of the ca_util routines can use to check if a callback
# has occurred for this PV.

class cadictEntry:
	def __init__(self, channel):
		self.channel = channel
		self.callbackReceived = 0	# reserved for use by caputw()
		self.field_type = channel.field_type()
		self.element_count = channel.element_count()
		#self.host_name = channel.host_name()

cadict = {}

#######################################################################
ca_utilExceptionStrings = ["No name was provided.", "Readback disagrees with put value.",
                           "PV is not connected."]
EXCEPTION_NULL_NAME          = 0
EXCEPTION_READBACK_DISAGREES = 1
EXCEPTION_NOT_CONNECTED      = 2

class ca_utilException(Exception):
	def __init__(self, *args):
		Exception.__init__(self, *args)
		self.errorNumber = args[0]

	def __int__(self):
		return int(self.errorNumber)

	def __str__(self):
		return ca_utilExceptionStrings[self.errorNumber]


#######################################################################
def convertToType(type, value):
	if type == ca.DBR_STRING:
		return str(value)
	elif type == ca.DBR_SHORT or type == ca.DBR_INT or type == ca.DBR_LONG:
		try:
			n = int(value)
		except:
			n = 0
		return n
	elif type == ca.DBR_FLOAT or type == ca.DBR_DOUBLE:
		try:
			n = float(value)
		except:
			n = 0.0
		return n
	elif type == ca.DBR_ENUM:
		return value
	elif type == ca.DBR_CHAR:
		return value
	else:
		return value

#######################################################################
def checkName(name, timeout=None, retries=None):
	"""
	usage: checkName("xxx:m1.VAL", timeout=None, retries=None)
	Intended for internal use by ca_util functions.
	"""

	global cadict, defaultTimeout, defaultRetries
	if not name:
		raise ca_utilException(EXCEPTION_NULL_NAME)

	if ((timeout == None) and (defaultTimeout != None)): timeout = defaultTimeout
	if (timeout == "NONE"): timeout = None

	if ((retries == None) and (defaultRetries != None)): retries = defaultRetries
	if ((retries == None) or (retries == "NONE")): retries = 0

	tries = 0
	while (name not in cadict) and (tries <= retries):
		# Make a new entry in the PV-name dictionary
		try:
			channel = CaChannel.CaChannel()
			if (timeout != None): channel.setTimeout(timeout)
			channel.searchw(name)
			cadict[name] = cadictEntry(channel)
		except CaChannel.CaChannelException:
			status = sys.exc_info()[1]
			del channel
			tries += 1

	if (name not in cadict):
		print("ca_util.checkName: Can't connect to '%s'" % name)
		raise CaChannel.CaChannelException(status)

#######################################################################
def castate(name=None, timeout=None, retries=None):
	"""usage: val = castate("xxx:m1.VAL", timeout=None, retries=None)
	   Try to read a PV, to find out whether it's really connected, and
	   whether caller is permitted to read and write it, without allowing
	   any exceptions to be thrown at the caller.
	"""

	global cadict, defaultTimeout, defaultRetries

	if not name: return "Null name has no state"

	# The only reliable way to check the *current* state of a PV is to attempt to use it.
	try:
		val = caget(name, timeout=timeout, retries=retries)
	except CaChannel.CaChannelException:
		pass

	try:
		checkName(name, timeout=timeout)
	except CaChannel.CaChannelException:
		return "not connected"
	except:
		return "error"

	try:
		state = cadict[name].channel.state()
	except CaChannel.CaChannelException:
		return "not connected"
	except:
		return "error"
	else:
		try:
			read_access = cadict[name].channel.read_access()
			write_access = cadict[name].channel.write_access()
			if state in ca_states:
				s = ca_states[state]
			else:
				s = "unknown state"
			if not read_access: s += ", noread"
			if not write_access: s += ", nowrite"
			return s
		except:
			return "error"

#######################################################################
def caget(name, timeout=None, retries=None, req_type=None, req_count=None):
	"""usage: val = caget("xxx:m1.VAL", timeout=None, retries=None,
	                req_type=None, req_count=None)"""

	global cadict, defaultTimeout, defaultRetries

	if not name:
		print("caget: no PV name supplied")
		raise ca_utilException(EXCEPTION_NULL_NAME)
	if ((timeout==None) and (defaultTimeout != None)): timeout = defaultTimeout
	if (timeout == "NONE"): timeout = None
	if ((retries==None) and (defaultRetries != None)): retries = defaultRetries
	if ((retries == None) or (retries == "NONE")): retries = 0
	retries = max(retries,0)
	retry = retries + 1
	success = 0

	# CaChannel sometimes chokes when it tries to process a channel that has been disconnected.
	# The simplest fix is to clear the channel and reconnect to the PV, which we can do cleanly
	# by deleting our dict entry for the channel, and calling checkName() to make a new entry.

	while ((not success) and (retry > 0)):
		checked = 0
		while ((not checked) and (retry > 0)):
			retry -= 1
			try:
				checkName(name, timeout=timeout)
			except CaChannel.CaChannelException:
				if retry <= 0:
					raise
			else:
				checked = 1

		entry = cadict[name]
		if (timeout != None): entry.channel.setTimeout(timeout)
		if req_type == None:
			req_type=entry.field_type
		# kludge for broken DBR_CHAR
		if req_type == ca.DBR_CHAR:
			req_type = ca.DBR_INT
		if req_count == None:
			req_count = entry.element_count
		req_count = max(0, min(req_count, entry.element_count))
		try:
			val = entry.channel.getw(req_type=req_type, count=req_count)
		except CaChannel.CaChannelException:
			status = sys.exc_info()[1]
			#print "getw threw an exception (%s)" % status
			if ((int(status) == ca.ECA_BADTYPE) or (int(status) == ca.ECA_DISCONN)):
				# Delete dictionary entry.  This clears the CA connection.
				print("caget: Repairing CA connection to ", name)
				del cadict[name]
				retry += 1
			if retry <= 0:
				raise
		else:
			success = 1
	return val

def isNumber(s):
	try:
		n = int(s)
	except:
		return False
	return True

#######################################################################
def same(value, readback, native_readback, field_type, read_check_tolerance):
	"""For internal use by ca_util"""
	#print "ca_util.same(): field_type=%s" % field_type
	#print "ca_util.same(): value='%s'; readback='%s', native_readback='%s'" % (str(value), str(readback), str(native_readback))
	#print "ca_util.same(): type(value)=%s; type(readback)=%s, type(native_readback)=%s" % (type(value),
	#	type(readback), type(native_readback))

	if field_type in [ca.DBR_FLOAT, ca.DBR_DOUBLE]:
		return (abs(float(readback)-float(value)) < read_check_tolerance)
	elif field_type in [ca.DBR_INT, ca.DBR_SHORT, ca.DBR_LONG]:
		return (abs(int(readback)-int(value)) == 0)
	elif field_type == ca.DBR_ENUM:
		if str(value) == str(readback):
			return True
		if str(value) == str(native_readback):
			return True
		return False
	else:
		return (str(value) == str(readback))

#######################################################################
def caput(name, value, timeout=None, req_type=None, retries=None, read_check_tolerance=None):
	"""
	usage: caput("xxx:m1.VAL", new_value, timeout=None, req_type=None,
	       retries=None, read_check_tolerance=None)
	Put a value, and optionally check that the value arrived safely.
	If read_check_tolerance == None (or is not supplied) then the default
	read-check tolerance is used.  If read_check_tolerance == "NONE", then no
	read check is done.
	If read_check_tolerance != "NONE", then floating point numbers must be
	closer than the tolerance, and other types must agree exactly.
	Note that defaults for timeout, retries, and read_check_tolerance can be
	set for all ca_util functions, using the command set_ca_util_defaults().
	"""

	_caput("caput", name, value, 0, timeout, req_type, retries, read_check_tolerance)


#######################################################################
def __ca_util_waitCB(epics_args, user_args):
	"""Function for internal use by caputw()."""
	#print "__ca_util_waitCB: %s done\n" % user_args[0]
	cadict[user_args[0]].callbackReceived = 1

#######################################################################
def caputw(name, value, wait_timeout=None, timeout=None, req_type=None, retries=None,
 read_check_tolerance=None):
	"""
	usage: caputw("xxx:m1.VAL", new_value, wait_timeout=None, timeout=None,
	       req_type=None, retries=None, read_check_tolerance=None)
	Put a value, optionally check that the value arrived safely, and wait (no
	longer than wait_timeout) for processing to complete. If
	read_check_tolerance == None (or is not supplied) then the default
	read-check tolerance is used.  If read_check_tolerance == "NONE", then no
	read check is done. If read_check_tolerance != "NONE", then floating point
	numbers must be closer than the tolerance, and other types must agree
	exactly. Note that defaults for timeout, retries, and read_check_tolerance
	can be set for all ca_util functions, using the command
	set_ca_util_defaults().
	"""

	_caput("caputw", name, value, wait_timeout, timeout, req_type, retries, read_check_tolerance)


#######################################################################
def _caput(function, name, value, wait_timeout=None, timeout=None, req_type=None, retries=None, read_check_tolerance=None):

	global cadict, defaultTimeout, defaultRetries, readCheckTolerance

	#print function
	if not name:
		print("%s: no PV name supplied" % function)
		raise ca_utilException(EXCEPTION_NULL_NAME)
	if ((timeout == None) and (defaultTimeout != None)): timeout = defaultTimeout
	if ((retries == None) and (defaultRetries != None)): retries = defaultRetries
	if ((retries == None) or (retries == "NONE")): retries = 0
	if ((read_check_tolerance == None) and (readCheckTolerance != None)):
		read_check_tolerance = readCheckTolerance

	retries = max(retries,0)
	retry = retries + 1
	success = 0

	checkName(name, timeout=timeout, retries=retries)

	while ((not success) and (retry > 0)):

		retry -= 1
		entry = cadict[name]

		state = castate(name, timeout)
		#print "%s: state='%s'" % (function, state)
		if (state != 'connected'):
			print("%s: Repairing CA connection to '%s'" % (function, name))
			del cadict[name]
			retry += 1
		else:
			if req_type == None:
				req_type=entry.field_type
			if ((timeout != None) and (timeout != "NONE")): entry.channel.setTimeout(timeout)
			entry.callbackReceived = 0 # in case we're doing caputw()
			#value = convertToType(value, req_type)
			try:
				if function == "caput":
					entry.channel.putw(value, req_type=req_type)
				else: #caputw
					retval = entry.channel.array_put_callback(value,req_type,entry.element_count,__ca_util_waitCB,name)
			except CaChannel.CaChannelException:
				status = sys.exc_info()[1]
				print("put() threw an exception (%s)" % status)
				if ((int(status) == ca.ECA_BADTYPE) or (int(status) == ca.ECA_DISCONN)):
					# Delete dictionary entry.  This clears the CA connection.
					print("%s: Repairing CA connection to '%s'" % (function, name))
					del cadict[name]
					retry += 1
				if retry <= 0:
					raise
					entry.callbackReceived = 1
					return
			else:
				if ((read_check_tolerance == None) or (read_check_tolerance == "NONE")):
					success = True
				else:
					if timeout:
						ca.pend_io(timeout)
					else:
						ca.pend_io(1.0)
					readback_success = False
					count = 0
					while ((not readback_success) and (count < retries+1)):
						try:
							readback = caget(name, req_type=req_type)
							native_readback = caget(name)
							readback_success = True
							if same(value, readback, native_readback, entry.field_type, read_check_tolerance):
								success = True
								#print "%s: Success\n" % (function)
							else:
								print("%s: readback '%s' disagrees with the value '%s' we wrote." % (function, readback, value))
								raise ca_utilException(EXCEPTION_READBACK_DISAGREES)
								entry.callbackReceived = 1
						except CaChannel.CaChannelException:
							print("%s: exception during readback." % (function))
							count += 1

	if success and (function == "caputw"):
		start_time = time.time()
		timed_out = 0
		while (not entry.callbackReceived) and (not timed_out):
			#print "waiting for ", name
			time.sleep(0.1)
			#ca.pend_io(0.1)
			ca.poll()
			if (not wait_timeout):
				timed_out = 0
			else:
				timed_out = ((time.time()-start_time) > wait_timeout)

		if not entry.callbackReceived:
			print("Execution not completed by wait_timeout (%d seconds)" % wait_timeout)

#######################################################################
def camonitor(name, function, user_args=None, timeout=None, retries=None):
	"""
	usage: camonitor("xxx:m1.VAL", python_function, user_args, timeout=None,
	       retries=None)
	Don't forget to call ca.pend_event(<pend_time_in_seconds>) periodically.
	"""

	global defaultTimeout, defaultRetries

	if not name:
		print("camonitor: no PV name supplied")
		raise ca_utilException(EXCEPTION_NULL_NAME)
	if not function:
		print("camonitor: no callback function supplied")
		raise ca_utilException(EXCEPTION_NULL_NAME)
	if not user_args: user_args = name
	if ((timeout==None) and (defaultTimeout != None)): timeout = defaultTimeout
	if ((retries==None) and (defaultRetries != None)): retries = defaultRetries
	if ((retries == None) or (retries == "NONE")): retries = 0

	retries = max(retries,0)
	retry = retries + 1
	success = 0

	while ((not success) and (retry > 0)):
		checked = 0
		while ((not checked) and (retry > 0)):
			retry -= 1
			try:
				checkName(name, timeout=timeout)
			except CaChannel.CaChannelException:
				if retry <= 0:
					raise
			else:
				checked = 1

		entry = cadict[name]
		if ((timeout != None) and (timeout != "NONE")): entry.channel.setTimeout(timeout)
		try:
			entry.channel.add_masked_array_event(entry.field_type,entry.element_count,ca.DBE_VALUE, function, user_args)
		except CaChannel.CaChannelException:
			status = sys.exc_info()[1]
			#print "add_masked_array_event threw an exception (%s)" % status
			if ((int(status) == ca.ECA_BADTYPE) or (int(status) == ca.ECA_DISCONN)):
				# Delete dictionary entry.  This clears the CA connection.
				print("camonitor: Repairing CA connection to %s" % name)
				del cadict[name]
				retry += 1
			if retry <= 0:
				raise
		else:
			success = 1

#######################################################################
def caunmonitor(name, timeout=None):
	"""usage: caunmonitor("xxx:m1.VAL", timeout=None)"""

	global defaultTimeout

	if not name:
		print("caunmonitor: no PV name supplied")
		raise ca_utilException(EXCEPTION_NULL_NAME)
	if ((timeout==None) and (defaultTimeout != None)): timeout = defaultTimeout

	if name not in cadict:
		print("ca_util has no connection to '%s'" % name)
		raise ca_utilException(EXCEPTION_NOT_CONNECTED)

	channel = cadict[name].channel
	if ((timeout != None) and (timeout != "NONE")): channel.setTimeout(timeout)
	try:
		channel.clear_event()
	except CaChannel.CaChannelException:
		status = sys.exc_info()[1]
		print("caunmonitor: CaChannel exception, status=%d (%s)" % (status, ca.message(status)))
		return

#######################################################################
def test_monitor_function(epics_args, user_args):
	"""Example callback routine for use with camonitor()."""
	print('test_monitor_function:')
	print("...epics_args: %s" % repr(epics_args))
	print("...user_args: %s" % repr(user_args))





#-------------------------------------------------------------------------------------------
# miscellaneous functions that might be useful, but haven't been integrated into the package

#######################################################################
def endianUs():
	"""
	usage: endianUs()
	Returns one of "Little Endian", "Big Endian", "Unknown Endian".
	"""

	from struct import pack
	if pack('<h', 1) == pack('=h',1):
		return "Little Endian"
	elif pack('>h', 1) == pack('=h',1):
		return "Big Endian"
	else:
		return "Unknown Endian"

#######################################################################
def printExceptionInfo(maxTBlevel=15):
	"""Intended for internal use by ca_util functions."""

	import sys, traceback
	cla, exc, trbk = sys.exc_info()
	excName = cla.__name__
	try:
		excArgs = exc.__dict__["args"]
	except KeyError:
		excArgs = "<no args>"
	excTb = traceback.format_tb(trbk, maxTBlevel)
	print("Unanticipated exception: %s %s\n" % (excName, excArgs))
	if (len(excTb) > 0):
		print("Traceback:")
		for trace in excTb:
			print(trace)
	return
