#!/usr/bin/env python

##############################################################################
##
# This file is part of Sardana
##
# http://www.sardana-controls.org/
##
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
##
# Sardana 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.
##
# Sardana 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 Sardana.  If not, see <http://www.gnu.org/licenses/>.
##
##############################################################################

""" """

__all__ = ["PseudoMotor", "PseudoMotorClass"]

__docformat__ = 'restructuredtext'

import sys
import time

from PyTango import DevFailed, Except, READ_WRITE, SCALAR, DevVoid, \
    DevDouble, DevBoolean, DevVarStringArray, DevVarDoubleArray, DevState, \
    AttrQuality

from taurus.core.util.log import DebugIt

from sardana import State, SardanaServer
from sardana.sardanaexception import SardanaException
from sardana.sardanaattribute import SardanaAttribute
from sardana.tango.core.util import exception_str, to_tango_type_format, \
    throw_sardana_exception
from sardana.tango.pool.PoolDevice import PoolElementDevice, \
    PoolElementDeviceClass


class PseudoMotor(PoolElementDevice):

    def __init__(self, dclass, name):
        self.in_write_position = False
        PoolElementDevice.__init__(self, dclass, name)

    def init(self, name):
        PoolElementDevice.init(self, name)

    def _is_allowed(self, req_type):
        return PoolElementDevice._is_allowed(self, req_type)

    def get_pseudo_motor(self):
        return self.element

    def set_pseudo_motor(self, pseudo_motor):
        self.element = pseudo_motor

    pseudo_motor = property(get_pseudo_motor, set_pseudo_motor)

    @DebugIt()
    def delete_device(self):
        PoolElementDevice.delete_device(self)
        pseudo_motor = self.pseudo_motor
        if pseudo_motor is not None:
            pseudo_motor.remove_listener(self.on_pseudo_motor_changed)

    @DebugIt()
    def init_device(self):
        PoolElementDevice.init_device(self)

        self.Elements = list(map(int, self.Elements))
        pseudo_motor = self.pseudo_motor
        if self.pseudo_motor is None:
            full_name = self.get_full_name()
            name = self.alias or full_name
            self.pseudo_motor = pseudo_motor = \
                self.pool.create_element(type="PseudoMotor", name=name,
                                         full_name=full_name, id=self.Id, axis=self.Axis,
                                         ctrl_id=self.Ctrl_id, user_elements=self.Elements)
            if self.instrument is not None:
                pseudo_motor.set_instrument(self.instrument)
        pseudo_motor.set_drift_correction(self.DriftCorrection)
        pseudo_motor.add_listener(self.on_pseudo_motor_changed)

        self.set_state(DevState.ON)

    def on_pseudo_motor_changed(self, event_source, event_type, event_value):
        try:
            self._on_pseudo_motor_changed(event_source, event_type,
                                          event_value)
        except:
            msg = 'Error occurred "on_pseudo_motor_changed(%s.%s): %s"'
            exc_info = sys.exc_info()
            self.error(msg, self.pseudo_motor.name, event_type.name,
                       exception_str(*exc_info[:2]))
            self.debug("Details", exc_info=exc_info)

    def _on_pseudo_motor_changed(self, event_source, event_type, event_value):
        # during server startup and shutdown avoid processing element
        # creation events
        if SardanaServer.server_state != State.Running:
            return

        timestamp = time.time()
        name = event_type.name.lower()

        try:
            attr = self.get_attribute_by_name(name)
        except DevFailed:
            return
        quality = AttrQuality.ATTR_VALID
        priority = event_type.priority
        value, w_value, error = None, None, None

        if name == "state":
            value = self.calculate_tango_state(event_value)
        elif name == "status":
            value = self.calculate_tango_status(event_value)
        else:
            if isinstance(event_value, SardanaAttribute):
                if event_value.error:
                    error = Except.to_dev_failed(*event_value.exc_info)
                else:
                    value = event_value.value
                timestamp = event_value.timestamp
            else:
                value = event_value
            state = self.pseudo_motor.get_state(propagate=0)

            if name == "position":
                w_value = event_value.w_value
                if state == State.Moving:
                    quality = AttrQuality.ATTR_CHANGING

        self.set_attribute(attr, value=value, w_value=w_value,
                           timestamp=timestamp, quality=quality,
                           priority=priority, error=error, synch=False)

    def always_executed_hook(self):
        #state = to_tango_state(self.pseudo_motor.get_state(cache=False))
        pass

    def read_attr_hardware(self, data):
        pass

    def get_dynamic_attributes(self):
        cache_built = hasattr(self, "_dynamic_attributes_cache")

        std_attrs, dyn_attrs = \
            PoolElementDevice.get_dynamic_attributes(self)

        if not cache_built:
            # For position attribute, listen to what the controller says for
            # data type (between long and float)
            pos = std_attrs.get('position')
            if pos is not None:
                _, data_info, attr_info = pos
                ttype, _ = to_tango_type_format(attr_info.dtype)
                data_info[0][0] = ttype
        return std_attrs, dyn_attrs

    def initialize_dynamic_attributes(self):
        attrs = PoolElementDevice.initialize_dynamic_attributes(self)

        detect_evts = "position",
        non_detect_evts = ()

        for attr_name in detect_evts:
            if attr_name in attrs:
                self.set_change_event(attr_name, True, True)
        for attr_name in non_detect_evts:
            if attr_name in attrs:
                self.set_change_event(attr_name, True, False)
        return

    def read_Position(self, attr):
        pseudo_motor = self.pseudo_motor
        use_cache = pseudo_motor.is_in_operation() and not self.Force_HW_Read
        position = pseudo_motor.get_position(cache=use_cache, propagate=0)
        if position.error:
            throw_sardana_exception(position)
        state = pseudo_motor.get_state(cache=use_cache, propagate=0)
        quality = None
        if state == State.Moving:
            quality = AttrQuality.ATTR_CHANGING
        self.set_attribute(attr, value=position.value, w_value=position.w_value,
                           quality=quality, priority=0,
                           timestamp=position.timestamp)

    def write_Position(self, attr):
        self.in_write_position = True
        try:
            position = attr.get_write_value()
            self.debug("write_Position(%s)", position)
            try:
                self.wait_for_operation()
            except:
                raise Exception("Cannot move: already in motion")
            pseudo_motor = self.pseudo_motor
            try:
                pseudo_motor.position = position
            except SardanaException as se:
                throw_sardana_exception(se)
        finally:
            self.in_write_position = False

    def CalcPseudo(self, physical_positions):
        """Returns the pseudo motor position for the given physical positions"""
        if not len(physical_positions):
            physical_positions = None
        result = self.pseudo_motor.calc_pseudo(
            physical_positions=physical_positions)
        if result.error:
            throw_sardana_exception(result)
        return result.value

    def CalcPhysical(self, pseudo_position):
        """Returns the physical motor positions for the given pseudo motor
        position assuming the current pseudo motor write positions for all the
        other sibling pseudo motors"""
        result = self.pseudo_motor.calc_physical(pseudo_position)
        if result.error:
            throw_sardana_exception(result)
        return result.value

    def CalcAllPhysical(self, pseudo_positions):
        """Returns the physical motor positions for the given pseudo motor
        position(s)"""
        result = self.pseudo_motor.calc_physical(pseudo_positions)
        if result.error:
            throw_sardana_exception(result)
        return result.value

    def CalcAllPseudo(self, physical_positions):
        """Returns the pseudo motor position(s) for the given physical positions"""
        result = self.pseudo_motor.calc_all_pseudo(physical_positions)
        if result.error:
            throw_sardana_exception(result)
        return result.value

    def MoveRelative(self, argin):
        raise NotImplementedError

    def is_MoveRelative_allowed(self):
        if self.get_state() in (DevState.FAULT, DevState.MOVING,
                                DevState.UNKNOWN):
            return False
        return True

    is_Position_allowed = _is_allowed


class PseudoMotorClass(PoolElementDeviceClass):

    #    Class Properties
    class_property_list = {
    }

    #    Device Properties
    device_property_list = {
        "Elements":    [DevVarStringArray, "elements used by the pseudo", []],
        'DriftCorrection':
            [DevBoolean,
             "Locally apply drift correction on pseudo motors. Default is the "
             "current global drift correction in the Pool Device",
             None],
    }
    device_property_list.update(PoolElementDeviceClass.device_property_list)

    #    Command definitions
    cmd_list = {
        'CalcPseudo': [[DevVarDoubleArray, "physical positions"], [DevDouble, "pseudo position"]],
        'CalcPhysical': [[DevDouble, "pseudo position"], [DevVarDoubleArray, "physical positions"]],
        'CalcAllPseudo': [[DevVarDoubleArray, "physical positions"], [DevVarDoubleArray, "pseudo positions"]],
        'CalcAllPhysical': [[DevVarDoubleArray, "pseudo positions"], [DevVarDoubleArray, "physical positions"]],
        'MoveRelative': [[DevDouble, "amount to move"], [DevVoid, ""]],
    }
    cmd_list.update(PoolElementDeviceClass.cmd_list)

    #    Attribute definitions
    standard_attr_list = {
        'Position': [[DevDouble, SCALAR, READ_WRITE],
                     {'label': "Position",
                      'abs_change': '1.0', }, ],
    }
    standard_attr_list.update(PoolElementDeviceClass.standard_attr_list)

    def _get_class_properties(self):
        ret = PoolElementDeviceClass._get_class_properties(self)
        ret['Description'] = "Pseudo motor device class"
        ret['InheritedFrom'].insert(0, 'PoolElementDevice')
        return ret
