#!/usr/bin/env python
# -*- coding:utf-8 -*-


# ############################################################################
#  license :
# ============================================================================
#
#  File :        TangoEpics.py
#
#  Project :     Tango device server for accessing EPICS Process Variables
#
# This file is part of Tango device class.
#
# Tango is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Tango is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Tango.  If not, see <http://www.gnu.org/licenses/>.
#
#
#  $Author :      jmoldes$
#
#  $Revision :    $
#
#  $Date :        $
#
#  $HeadUrl :     $
# ============================================================================
#            This file is generated by POGO
#     (Program Obviously used to Generate tango Object)
# ############################################################################

import PyTango
import sys

# Add additional import
# ----- PROTECTED REGION ID(TangoEpics.additionnal_import) ENABLED START -----#
import epics
# ----- PROTECTED REGION END -----#    //    TangoEpics.additionnal_import

__all__ = ["TangoEpics", "TangoEpicsClass", "main"]

__docformat__ = 'restructuredtext'


# Device States Description
# FAULT : Invalid properties
# ON :


class TangoEpics (PyTango.Device_4Impl):
    """Tango device server for accessing Epics Process Variables.

    You must provide the list of PVs you want to interface and optionally a
     host name and then the device will create a tango attribute for each PV.

    Take into account that Tango and Epics follow different naming conventions,
    which may cause problems. In order to allow maximum flexibility all options
    have been left open in order to translate the Epics process variable names
    to tango attributes names:

    1) Name the tango attribute exactly as the Epics process variable name.
       Take into account that this may cause problems if process variables
       names include special characters.

    2) Name the tango attribute just like the Epics process variable name but
       replacing the special characters by others. The replacement may be:
      a- Automatic: all special characters are replace by a _ character
      b- Manual: you can specify replacement strings for special characters

    3) Give your own names to tango attributes.

    See properties section for more details on how to configure all these
    options.

    Note that if your Epics IOC contains process variables named State and/or
    Status these will compulsory have to be renamed.

    The server uses PyEpics for interfacing Epics. See:
    http://cars9.uchicago.edu/software/python/pyepics3/index.html"""

    # -------- Add you global variables here --------------------------
    # ----- PROTECTED REGION ID(TangoEpics.global_variables) ENABLED START ---#
    # ----- PROTECTED REGION END -----#	//	TangoEpics.global_variables

    def __init__(self, cl, name):
        PyTango.Device_4Impl.__init__(self, cl, name)
        self.debug_stream("In __init__()")
        self.pv = None
        TangoEpics.init_device(self)
        # ----- PROTECTED REGION ID(TangoEpics.__init__) ENABLED START -----#
        # ----- PROTECTED REGION END -----#	//	TangoEpics.__init__

    def delete_device(self):
        self.debug_stream("In delete_device()")
        # ----- PROTECTED REGION ID(TangoEpics.delete_device) ENABLED START --#
        self.pv = None
        # ----- PROTECTED REGION END -----#	//	TangoEpics.delete_device

    def init_device(self):
        self.debug_stream("In init_device()")
        self.get_device_properties(self.get_device_class())
        # ----- PROTECTED REGION ID(TangoEpics.init_device) ENABLED START ----#
        self._set_state(PyTango.DevState.INIT, 'Initializing', force_init=True)
        self.initialize_dynamic_attributes()
        self._set_state(PyTango.DevState.ON, 'All PVs accesible')
        # ----- PROTECTED REGION END -----#	//	TangoEpics.init_device

    def always_executed_hook(self):
        self.debug_stream("In always_excuted_hook()")
        # ----- PROTECTED REGION ID(TangoEpics.always_executed_hook)
        # ENABLED START -----#

        # ----- PROTECTED REGION END -----#	//	TangoEpics.always_executed_hook

    # -------------------------------------------------------------------------
    #    TangoEpics read/write attribute methods
    # -------------------------------------------------------------------------

    def read_attr_hardware(self, data):
        self.debug_stream("In read_attr_hardware()")
        # ----- PROTECTED REGION ID(TangoEpics.read_attr_hardware) ENABLED
        # START -----#
        if self.pv is None:  # nothing to do if not initialized
            return
        try:
            epics.ca.use_initial_context()
            # read all PVs
            for attr_idx in data:
                attr_name = \
                    self.get_device_attr().get_attr_by_ind(attr_idx).get_name()
                ch_id = self.pv[attr_name][0]
                # we don't wait until we get the value:
                # will be done once for all
                epics.ca.get(ch_id, wait=False)
            # wait for reading completion of all PVs
            epics.ca.poll()
        except Exception, e:
            msg = 'Error while reading PVs. Please check communication.'
            self.error_stream('%s:\n%s' % (msg, str(e)))
            self._set_state(PyTango.DevState.ALARM, msg)
        # ----- PROTECTED REGION END -----#	//	TangoEpics.read_attr_hardware

    # -------------------------------------------------------------------------
    #    TangoEpics command methods
    # -------------------------------------------------------------------------

    def getHost(self):
        """ Returns the value of the optional property Host
        :return: Value of the optional property Host
        :rtype: PyTango.DevString
        """
        self.debug_stream("In getHost()")
        argout = ""
        # ----- PROTECTED REGION ID(TangoEpics.getHost) ENABLED START -----#
        argout = self.Host
        # ----- PROTECTED REGION END -----#	//	TangoEpics.getHost
        return argout

    def getTangoName(self, argin):
        """ Returns the Tango attribute name for an Epics process variable name.
        Take into account that the device is case sensitive
        :param argin: Epics process variable name
        :type argin: PyTango.DevString
        :return: Tango attribute name for the Epics process variable name
        :rtype: PyTango.DevString
        """
        self.debug_stream("In getTangoName()")
        argout = ""
        # ----- PROTECTED REGION ID(TangoEpics.getTangoName) ENABLED START ---#
        if self.Host != '' and not argin.startswith(self.Host):
            argin = '%s:%s' % (self.Host, argin)
        for attr_name, data in self.pv.items():
            if argin == data[1]:
                argout = attr_name
                break
        # ----- PROTECTED REGION END -----#	//	TangoEpics.getTangoName
        return argout

    def getEpicsName(self, argin):
        """ Returns Epics process variable name for a Tango attribute name.
        Take into account that the device is case sensitive
        :param argin: Tango attribute name
        :type argin: PyTango.DevString
        :return: Epics process variable name
        :rtype: PyTango.DevString
        """
        self.debug_stream("In getEpicsName()")
        argout = ""
        # ----- PROTECTED REGION ID(TangoEpics.getEpicsName) ENABLED START ---#
        try:
            argout = self.pv[argin][1]
            argout = argout.replace('%s:' % self.Host, '')  # remove host part
        except KeyError:  # attribute doesn't exist: simply return empty string
            pass
        except Exception, e:
            msg = 'Unexpected error:\n%s' % str(e)
            self.error_stream(msg)
            PyTango.Except.throw_exception(
                'Get Epics name',
                msg,
                'getEpicsName')
        # ----- PROTECTED REGION END -----#	//	TangoEpics.getEpicsName
        return argout

    # ----- PROTECTED REGION ID(TangoEpics.programmer_methods) ENABLED START -#
    def initialize_dynamic_attributes(self):
        self.debug_stream('In initialize_dynamic_attributes()')
        if self.pv is not None:  # already initialized
            return
        # variables initialization
        READ_METHOD = self.read_attr
        WRITE_METHOD = self.write_attr
        if self.Host == []:
            self.Host = ''
        self.pv = {}
        pvs = {}

        try:
            # initialize libca library
            epics.ca.initialize_libca()

            # get pv names and build libca channels
            for pv_entry in self.ProcessVariables:
                pv_attr = pv_entry.split('\t')
                attr_name = None
                if len(pv_attr) == 1:
                    pv_name = pv_attr[0]
                elif len(pv_attr) == 2:
                    pv_name, attr_name = pv_attr
                else:
                    msg = 'Invalid entry in ProcessVariables device property:'\
                        ' %s' % pv_entry
                    self.error_stream(msg)
                    self._set_state(PyTango.DevState.FAULT, msg)
                    return
                # get host name if necessary
                if self.Host != '':
                    pv_full_name = '%s:%s' % (self.Host, pv_name)
                else:
                    pv_full_name = pv_name
                # create epics channel to PV (to increase performance do not
                # wait for a connection)
                ch_id = epics.ca.create_channel(
                    pv_full_name,
                    connect=False,
                    auto_cb=False)
                # build default attribute name (if not provided)
                if attr_name is None:
                    attr_name = self.epics2tango_name(pv_name)
                self.pv[attr_name] = [ch_id, pv_full_name]
                pvs[attr_name] = [ch_id]

            # connect PVs and get necessary info
            for name, data in pvs.items():
                connected = epics.ca.connect_channel(data[0])
                if not connected:
                    msg = 'At least one channel unreachable. Is hardware up?'
                    self.error_stream(msg)
                    self._set_state(PyTango.DevState.FAULT,
                                    msg, force_init=True)
                    return
            epics.ca.poll()  # wait for connections completion
            for name, data in pvs.items():
                data.append(epics.ca.field_type(data[0]))
                data.append(epics.ca.read_access(data[0]))
                data.append(epics.ca.write_access(data[0]))
                data.append(epics.ca.element_count(data[0]))

            # add dynamic attributes
            for attr_name, data in pvs.items():
                # find out tango type
                tango_type = self.epics2tango_type(data[1])
                if tango_type is None:
                    msg = 'Unknown type %s for PV %s' %\
                          (str(data[1]), self.pv[attr_name][1])
                    self.error_stream(msg)
                    self._set_state(PyTango.DevState.ALARM, msg)
                    del self.pv[attr_name]
                    continue
                # find out read/write access
                read_access = bool(data[2])
                write_access = bool(data[3])
                read_method = None
                write_method = None
                if read_access and write_access:
                    read_method = READ_METHOD
                    write_method = WRITE_METHOD
                    access_type = PyTango.AttrWriteType.READ_WRITE
                elif read_access and not write_access:
                    read_method = READ_METHOD
                    access_type = PyTango.AttrWriteType.READ
                elif not read_access and write_access:
                    write_method = WRITE_METHOD
                    access_type = PyTango.AttrWriteType.WRITE
                else:
                    msg = 'PV %s has neither read nor write permissions'\
                          % pv_full_name
                    self.error_stream(msg)
                    self._set_state(PyTango.DevState.FAULT, msg)
                    continue
                # find out dimension and build attribute
                dimension = data[4]
                if dimension > 1:
                    attr = PyTango.SpectrumAttr(
                        attr_name,
                        tango_type,
                        access_type,
                        dimension)
                else:
                    attr = PyTango.Attr(
                        attr_name,
                        tango_type,
                        access_type)
                properties = PyTango.UserDefaultAttrProp()
                # set description field with PV name
                properties.set_description(self.pv[attr_name][1])
                attr.set_default_properties(properties)
                # add attribute
                self.add_attribute(attr, read_method, write_method)
        except Exception, e:
            msg = 'Unable to create dynamic attributes'
            self.error_stream('%s: %s' % (msg, str(e)))
            self._set_state(PyTango.DevState.FAULT, msg)
        del pvs  # no longer used

    def remove_dynamic_attributes(self):
        try:
            for attr_name in self.pv.keys():
                self.remove_attribute(attr_name)
                del self.pv[attr_name]
        except Exception, e:
            self.error_stream(str(e))

    def read_attr(self, attr):
        attr_name = attr.get_name()
        self.debug_stream('[read_attr] attribute %s' % attr_name)
        try:
            epics.ca.use_initial_context()
            ch_id = self.pv[attr_name][0]
            # reading of PVs was requested on read_attr_hardware(): now only
            value = epics.ca.get_complete(ch_id)        # retrieve the value
            if value is None:  # reading failed
                attr.set_quality(PyTango.AttrQuality.ATTR_INVALID)
                msg = 'Error reading attribute %s' % attr_name
                self.error_stream(msg)
                self._set_state(PyTango.DevState.ALARM, msg)
        except Exception, e:
            msg = 'Error reading attribute %s' % attr_name
            self.error_stream('%s: %s' % (msg, str(e)))
            self._set_state(PyTango.DevState.ALARM, msg)
            PyTango.Except.throw_exception(
                'Read error',
                msg,
                '%s.read_%s()' % (self.__class__.__name__, attr_name))
        else:
            attr.set_value(value)

    def write_attr(self, attr):
        attr_name = attr.get_name()
        self.debug_stream('[write_attr] attribute %s' % attr_name)
        try:
            value = attr.get_write_value()
            epics.ca.use_initial_context()
            ch_id = self.pv[attr_name][0]
            epics.ca.put(ch_id, value, wait=True)
        except Exception, e:
            msg = 'Error writing attribute %s' % attr_name
            self.error_stream('%s: %s' % (msg, str(e)))
            PyTango.Except.throw_exception(
                'Write error',
                msg,
                '%s.write_%s()' % (self.__class__.__name__, attr_name))

    def _set_state(self, new_state, new_status=None, force_init=False):
        current_state = self.get_state()
        # don't allow to change FAULT state unless initialing
        if (current_state in [PyTango.DevState.FAULT]) and not force_init:
            return
        if new_state != current_state:
            self.set_state(new_state)
        if new_status is not None:
            self.set_status(new_status)

    def epics2tango_name(self, pv_name):
        """PV names allow special character not available in tango"""
        tango_name = ''
        epics_dict = {}
        # build dictionary
        for string in self.AutoTranslation:
            if len(string) < 2:
                continue
            epics_dict[string[0]] = string[1:]
        # replace keys
        while len(pv_name) > 0:
            key_found = False
            # works because keys are single chars (otherwise reg exp needed)
            for key in epics_dict.keys():
                if pv_name.startswith(key):
                    key_found = True
                    break
            if key_found:
                tango_name += epics_dict[key]
                pv_name = pv_name[len(key):]
            else:
                tango_name += pv_name[0]
                pv_name = pv_name[1:]
        return tango_name

    def epics2tango_type(self, epics_type):
        """
        According to
        http://cars9.uchicago.edu/software/python/pyepics3/ca.html:
        string        0     string
        int           1     integer
        short         1     integer
        float         2     double
        enum          3     integer
        char          4     byte
        long          5     integer
        double        6     double
        time_string   14
        time_int      15
        time_short    15
        time_float    16
        time_enum     17
        time_char     18
        time_long     19
        time_double   20
        ctrl_string   28
        ctrl_int      29
        ctrl_short    29
        ctrl_float    30
        ctrl_enum     31
        ctrl_char     32
        ctrl_long     33
        ctrl_double   34
        """
        if epics_type == 0:
            type_ = PyTango.DevString
        elif epics_type == 1:
            type_ = PyTango.DevLong
        elif epics_type == 2:
            type_ = PyTango.DevFloat
        elif epics_type == 3:
            type_ = PyTango.DevULong
        elif epics_type == 4:
            type_ = PyTango.DevUChar
        elif epics_type == 5:
            type_ = PyTango.DevLong
        elif epics_type == 6:
            type_ = PyTango.DevDouble
        elif epics_type == 14:
            type_ = PyTango.DevString
        elif epics_type == 15:
            type_ = PyTango.DevLong
        elif epics_type == 16:
            type_ = PyTango.DevFloat
        elif epics_type == 17:
            type_ = PyTango.DevULong
        elif epics_type == 18:
            type_ = PyTango.DevUChar
        elif epics_type == 19:
            type_ = PyTango.DevLong
        elif epics_type == 20:
            type_ = PyTango.DevDouble
        elif epics_type == 28:
            type_ = PyTango.DevString
        elif epics_type == 29:
            type_ = PyTango.DevLong
        elif epics_type == 30:
            type_ = PyTango.DevFloat
        elif epics_type == 31:
            type_ = PyTango.DevULong
        elif epics_type == 32:
            type_ = PyTango.DevUChar
        elif epics_type == 33:
            type_ = PyTango.DevLong
        elif epics_type == 34:
            type_ = PyTango.DevDouble
        else:
            type_ = None
        return type_
    # ----- PROTECTED REGION END -----#	//	TangoEpics.programmer_methods


class TangoEpicsClass(PyTango.DeviceClass):
    # -------- Add you global class variables here --------------------------
    # ----- PROTECTED REGION ID(TangoEpics.global_class_variables) ENABLED
    # START -----#
    def dyn_attr(self, dev_list):
        for dev in dev_list:
            try:
                dev.initialize_dynamic_attributes()
            except:
                import traceback
                dev.warn_stream("Failed to initialize dynamic attributes")
                dev.debug_stream("Details: " + traceback.format_exc())
    # ----- PROTECTED REGION END -----#	//	TangoEpics.global_class_variables

    #    Class Properties
    class_property_list = {
        }

    #    Device Properties
    device_property_list = {
        'ProcessVariables':
            [PyTango.DevVarStringArray,
             "This is the list of EPICS process variables that we want to "
             "interface and,\noptionally ,the name we want to give to their "
             "corresponding tango attribute.\n\nTake into account that naming "
             "conventions in Tango and Epics are different.\nEpics pv reserved"
             " characters and keywords (Epics Application Developer?s Guide):"
             "\n - See 6.3.1 Keywords and 6.3.2 Unquoted strings of \nTango "
             "reserved characters and strings:\n - Appendix C.4 Reserved words"
             " and characters, limitations\n\nEach row has the following "
             "format (TAB means the tabulator character):"
             "\nepics_pv_nameTABtango_attr_name\n\n"
             "The use of this optional tango name opens two possibilities:\n\n"
             "1) If a ``tango_attr_name`` is provided then it will be used as "
             "the tango attribute\nname corresping to the pv name, no matter "
             "which characters or keywords may be.\n\n2) If no "
             "``TABtango_attr_name`` is given then the corresponding tango "
             "attribute will\nbe named exactly as the epics process variable "
             "BUT special characters may\noptionally be replaced in the tango "
             "attribute name. This gives 3 options:\n\n  a- Do nothing and "
             "name the tango attribute exactly as the PV name. Take into\n    "
             " account that this may cause problems with tango. In order to do"
             " this you\n     have to define the AutoTranslate property and "
             "leave it empty (otherwise\n     default translation characters "
             "will be used).\n\n  b- Replace it with any string that you "
             "decide. To do so you have to specify\n     the replacement "
             "string in the AutoTranslate property.\n\n  c- Replace it with "
             "character _. This is the default behaviour (default values\n    "
             " of AutoTranslate property do this)\n\nRemember that in any case"
             " the original PV name is set as the corresponding tango\n"
             "attribute description field of its default properties (which you"
             " can easily\nretrieve by getting attribute`s AttributeInfo with"
             " attribute_query() method)\nFurthermore you can use the "
             "getEpicsName and getTangoName functions to find out\nthe "
             "correspondence between epic pv name and tango attribute name.",
             []],
        'Host':
            [PyTango.DevString,
             "Optional host name to prepend to all PV names",
             []],
        'AutoTranslation':
            [PyTango.DevVarStringArray,
             "Array of strings for special characters replacement. Each string"
             " consist on a\nfirst character (this is the epics special "
             "character) and at least another\ncharacter (may be more), which "
             "will replace the epics special character in the\ntango attribute"
             " name. For example you could replace the epics : character with"
             "\nthe string _colon_ by providing the replacement "
             "string `:_colon_`",
             ["__", "-_", ":_", "._", "[_", "]_", "<_", ">_", ";_"]],
        }

    #    Command definitions
    cmd_list = {
        'getHost':
            [[PyTango.DevVoid, "none"],
             [PyTango.DevString, "Value of the optional property Host"]],
        'getTangoName':
            [[PyTango.DevString, "Epics process variable name"],
             [PyTango.DevString, "Tango attribute name for the Epics process"
                                 "variable name"]],
        'getEpicsName':
            [[PyTango.DevString, "Tango attribute name"],
             [PyTango.DevString, "Epics process variable name"]],
        }

    #    Attribute definitions
    attr_list = {
        }


def main():
    try:
        py = PyTango.Util(sys.argv)
        py.add_class(TangoEpicsClass, TangoEpics, 'TangoEpics')
        # ----- PROTECTED REGION ID(TangoEpics.add_classes) ENABLED START ----#

        # ----- PROTECTED REGION END -----#	//	TangoEpics.add_classes

        U = PyTango.Util.instance()
        U.server_init()
        U.server_run()

    except PyTango.DevFailed as e:
        print ('-------> Received a DevFailed exception:', e)
    except Exception as e:
        print ('-------> An unforeseen exception occured....', e)

if __name__ == '__main__':
    main()
