#!/usr/bin/env python

#############################################################################
##
# This file is part of Taurus
##
# http://taurus-scada.org
##
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
##
# Taurus is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
##
# Taurus 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 Lesser General Public License for more details.
##
# You should have received a copy of the GNU Lesser General Public License
# along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
##
#############################################################################

"""This module defines the TangoDevice object"""

__all__ = ["TangoDevice"]

__docformat__ = "restructuredtext"

import time
from PyTango import (DeviceProxy, DevFailed, LockerInfo, DevState)

from taurus.core.taurusdevice import TaurusDevice
from taurus.core.taurusbasetypes import (TaurusDevState, TaurusLockInfo,
                                         LockStatus, TaurusEventType)
from taurus.core.util.log import taurus4_deprecation


class _TangoInfo(object):

    def __init__(self):
        self.dev_class = self.dev_type = 'TangoDevice'
        self.doc_url = 'http://www.esrf.fr/computing/cs/tango/tango_doc/ds_doc/'
        self.server_host = 'Unknown'
        self.server_id = 'Unknown'
        self.server_version = 1


class TangoDevice(TaurusDevice):
    """A Device object representing an abstraction of the PyTango.DeviceProxy
       object in the taurus.core.tango scheme"""

    # helper class property that stores a reference to the corresponding
    # factory
    _factory = None
    _scheme = 'tango'
    _description = "A Tango Device"

    def __init__(self, name, **kw):
        """Object initialization."""
        self.call__init__(TaurusDevice, name, **kw)
        self._deviceObj = self._createHWObject()
        self._lock_info = TaurusLockInfo()
        self._deviceStateObj = None
        # TODO reimplement using the new codification
        self._deviceState = TaurusDevState.Undefined

    # Export the DeviceProxy interface into this object.
    # This way we can call for example read_attribute on an object of this
    # class
    def __getattr__(self, name):
        if self._deviceObj is not None:
            return getattr(self._deviceObj, name)
        cls_name = self.__class__.__name__
        raise AttributeError("'%s' has no attribute '%s'" % (cls_name, name))

    # def __setattr__(self, name, value):
    #     if '_deviceObj' in self.__dict__ and self._deviceObj is not None:
    #         return setattr(self._deviceObj, name, value)
    #     super(TaurusDevice, self).__setattr__(name, value)

    def __contains__(self, key):
        """delegate the contains interface to the device proxy"""
        hw = self.getDeviceProxy()
        if hw is None:
            return False
        return hw.__contains__(key)

    def __getitem__(self, key):
        """read attribute value using key-indexing syntax (e.g. as in a dict)
        on the device"""
        attr = self.getAttribute(key)
        return attr.read()

    def __setitem__(self, key, value):
        """set attribute value using key-indexing syntax (e.g. as in a dict)
        on the device"""
        attr = self.getAttribute(key)
        return attr.write(value)

    def getAttribute(self, attrname):
        """Returns the attribute object given its name"""
        slashnb = attrname.count('/')
        if slashnb == 0:
            attrname = "%s/%s" % (self.getFullName(), attrname)
        elif attrname[0] == '/':
            attrname = "%s%s" % (self.getFullName(), attrname)
        return self.factory().getAttribute(attrname)

    @taurus4_deprecation(alt='.stateObj.read().rvalue [Tango] or ' +
                           '.state [agnostic]')
    def getState(self, cache=True):
        stateAttrValue = self.stateObj.read(cache=cache)
        if not stateAttrValue is None:
            state_rvalue = stateAttrValue.rvalue
            return DevState.values[state_rvalue.value]
        return None

    @taurus4_deprecation(alt='.stateObj [Tango] or ' +
                           '.factory.getAttribute(state_full_name) [agnostic]')
    def getStateObj(self):
        return self.stateObj

    @taurus4_deprecation(alt="state")
    def getSWState(self, cache=True):
        raise Exception('getSWState has been removed. Use state instead')
        # return self.getValueObj().rvalue

    @property
    def state(self, cache=True):
        """Reimplemented from :class:`TaurusDevice` to use Tango's state
        attribute for diagnosis of the current state. It supports a "cache"
        kwarg

        :param cache: (bool) If True (default), cache will be used when reading
                      the state attribute of this device

        :return: (TaurusDevState)
        """
        self._deviceState = TaurusDevState.NotReady
        try:
            taurus_tango_state = self.stateObj.read(cache).rvalue
        except:
            try:
                if self.getDeviceProxy().import_info().exported:
                    self._deviceState = TaurusDevState.Undefined
                    return self._deviceState  # Undefined
                else:
                    return self._deviceState  # NotReady
            except:
                return self._deviceState  # NotReady
        from taurus.core.tango.enums import DevState as TaurusTangoDevState
        if taurus_tango_state == TaurusTangoDevState.UNKNOWN:
            self._deviceState = TaurusDevState.Undefined
        elif taurus_tango_state not in (TaurusTangoDevState.FAULT,
                                      TaurusTangoDevState.DISABLE,
                                      TaurusTangoDevState.INIT):
            self._deviceState = TaurusDevState.Ready
        return self._deviceState

    @taurus4_deprecation(alt="state [agnostic] or stateObj.read [Tango]")
    def getValueObj(self, cache=True):
        """ Deprecated by TEP14.
        ..warning::
            this bck-compat implementation is not perfect because the
            rvalue of the returned TangoAttributeValue is now a member of
            TaurusDevState instead of TaurusSWDevState
        """
        from taurus.core.tango.tangoattribute import TangoAttrValue
        ret = TangoAttrValue()
        ret.rvalue = self.state(cache)
        return ret

    def getDisplayDescrObj(self, cache=True):
        desc_obj = super(TangoDevice, self).getDisplayDescrObj(cache)
        # extend the info on dev state
        ret = []
        for name, value in desc_obj:
            if name.lower() == 'device state' and self.stateObj is not None:
                tg_state = self.stateObj.read(cache).rvalue.name
                value = "%s (%s)" % (value, tg_state)
            ret.append((name, value))
        return ret

    def cleanUp(self):
        self.trace("[TangoDevice] cleanUp")
        self._descr = None

        if not self._deviceStateObj is None:
            self._deviceStateObj.removeListener(self)
        self._deviceStateObj = None
        self._deviceObj = None
        TaurusDevice.cleanUp(self)

    @taurus4_deprecation(alt='.state().name')
    def getDisplayValue(self, cache=True):
        return self.state(cache).name

    def _createHWObject(self):
        try:
            return DeviceProxy(self.getFullName())
        except DevFailed, e:
            self.warning('Could not create HW object: %s' % (e[0].desc))
            self.traceback()

    @taurus4_deprecation(alt="getDeviceProxy()")
    def getHWObj(self):
        return self.getDeviceProxy()

    def getDeviceProxy(self):
        if self._deviceObj is None:
            self._deviceObj = self._createHWObject()
        return self._deviceObj

    @taurus4_deprecation(alt='getDeviceProxy() is not None')
    def isValidDev(self):
        """see: :meth:`TaurusDevice.isValid`"""
        return self._deviceObj is not None

    def lock(self, force=False):
        li = self.getLockInfo()
        if force:
            if self.getLockInfo().status == TaurusLockInfo.Locked:
                self.unlock(force=True)
        return self.getDeviceProxy().lock()

    def unlock(self, force=False):
        return self.getDeviceProxy().unlock(force)

    def getLockInfo(self, cache=False):
        lock_info = self._lock_info
        if cache and lock_info.status != LockStatus.Unknown:
            return lock_info
        try:
            dev = self.getDeviceProxy()
            li = LockerInfo()
            locked = dev.get_locker(li)
            msg = "%s " % self.getSimpleName()
            if locked:
                lock_info.id = pid = li.li
                lock_info.language = li.ll
                lock_info.host = host = li.locker_host
                lock_info.klass = li.locker_class
                if dev.is_locked_by_me():
                    status = LockStatus.LockedMaster
                    msg += "is locked by you!"
                else:
                    status = LockStatus.Locked
                    msg += "is locked by PID %s on %s" % (pid, host)
            else:
                lock_info.id = None
                lock_info.language = None
                lock_info.host = host = None
                lock_info.klass = None
                status = LockStatus.Unlocked
                msg += "is not locked"
            lock_info.status = status
            lock_info.status_msg = msg
        except:
            self._lock_info = lock_info = TaurusLockInfo()
        return lock_info

    #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-
    # Protected implementation
    #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-

    def removeListener(self, listener):
        ret = TaurusDevice.removeListener(self, listener)
        if not ret or self.hasListeners():
            return ret  # False, None or True
        return self.stateObj.removeListener(self)

    def addListener(self, listener):
        weWereListening = self.hasListeners()
        ret = TaurusDevice.addListener(self, listener)
        if not ret:
            return ret
        # We are only listening to State if someone is listening to us
        if weWereListening:
            # We were listening already, so we must fake an event to the new
            # subscribed listener with the current value
            evt_value = self.__decode(self.stateObj.read())
            listeners = hasattr(listener, '__iter__') and listener or [
                listener]
            self.fireEvent(TaurusEventType.Change, evt_value, listeners)
        else:
            # We were not listening to events, but now we have to
            self.stateObj.addListener(self)
        return ret

    def eventReceived(self, event_src, event_type, event_value):
        if event_type == TaurusEventType.Config:
            return
        value = self.__decode(event_value)
        new_state = value.rvalue
        if new_state != self._deviceState:
            msg = "Device State changed %s -> %s" % (self._deviceState.name,
                                                     new_state.name)
            self.debug(msg)
            self._deviceState = new_state
            self.fireEvent(TaurusEventType.Change, value)

    def __decode(self, event_value):
        """Decode events from the state attribute into TangoAttrValues whose
        rvalue is the Device state"""
        from taurus.core.tango.tangoattribute import TangoAttrValue
        if isinstance(event_value, TangoAttrValue):  # for change events (&co)
            new_state = TaurusDevState.Ready
        elif isinstance(event_value, DevFailed):  # for error events
            new_state = TaurusDevState.NotReady
        else:
            self.info("Unexpected event value: %r", event_value)
            new_state = TaurusDevState.Undefined
        from taurus.core.taurusbasetypes import TaurusModelValue
        value = TaurusModelValue()
        value.rvalue = new_state
        return value

    def __pollResult(self, attrs, ts, result, error=False):
        if error:
            for attr in attrs.values():
                attr.poll(single=False, value=None, error=result, time=ts)
            return

        for da in result:
            if da.has_failed:
                v, err = None, DevFailed(*da.get_err_stack())
            else:
                v, err = da, None
            attr = attrs[da.name]
            attr.poll(single=False, value=v, error=err, time=ts)

    def __pollAsynch(self, attrs):
        ts = time.time()
        try:
            req_id = self.read_attributes_asynch(attrs.keys())
        except DevFailed as e:
            return False, e, ts
        return True, req_id, ts

    def __pollReply(self, attrs, req_id, timeout=None):
        ok, req_id, ts = req_id
        if not ok:
            self.__pollResult(attrs, ts, req_id, error=True)
            return

        if timeout is None:
            timeout = 0
        timeout = int(timeout * 1000)
        result = self.read_attributes_reply(req_id, timeout)
        self.__pollResult(attrs, ts, result)

    def poll(self, attrs, asynch=False, req_id=None):
        '''optimized by reading of multiple attributes in one go'''
        if req_id is not None:
            return self.__pollReply(attrs, req_id)

        if asynch:
            return self.__pollAsynch(attrs)

        error = False
        ts = time.time()
        try:
            result = self.read_attributes(attrs.keys())
        except DevFailed as e:
            error = True
            result = e
        self.__pollResult(attrs, ts, result, error=error)

    def _repr_html_(self):
        try:
            info = self.getDeviceProxy().info()
        except:
            info = _TangoInfo()
        txt = """\
<table>
    <tr><td>Short name</td><td>{simple_name}</td></tr>
    <tr><td>Standard name</td><td>{normal_name}</td></tr>
    <tr><td>Full name</td><td>{full_name}</td></tr>
    <tr><td>Device class</td><td>{dev_class}</td></tr>
    <tr><td>Server</td><td>{server_id}</td></tr>
    <tr><td>Documentation</td><td><a target="_blank" href="{doc_url}">{doc_url}</a></td></tr>
</table>
""".format(simple_name=self.getSimpleName(), normal_name=self.getNormalName(),
           full_name=self.getFullName(), dev_class=info.dev_class,
           server_id=info.server_id, doc_url=info.doc_url)
        return txt

    @taurus4_deprecation(alt=".description")
    def getDescription(self, cache=True):
        return self.description

    @property
    def description(self):
        try:
            self._description = self.getDeviceProxy().description()
        except:
            pass
        return self._description

    @property
    def stateObj(self):
        if self._deviceStateObj is None:
            self._deviceStateObj = self.getAttribute("state")
        return self._deviceStateObj
