# *****************************************************************************
# This program 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 2 of the License, or (at your option) any later
# version.
#
# This program 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
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Module authors:
#   Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
# *****************************************************************************
"""Temperature Controller TC1 Quantum NorthWest"""

from frappy.core import Readable, Parameter, FloatRange, IDLE, ERROR, BoolType,\
    StringIO, HasIO, Property, WARN, Drivable, BUSY, StringType, Done
from frappy.errors import InternalError


class QnwIO(StringIO):
    """communication with TC1"""
    end_of_line = ']'  # no line feed!
    identification = [('[F1 VN ?', r'.F1 VN .*')]  # Controller Firmware Version, holder number


class SensorTC1(HasIO, Readable):
    ioClass = QnwIO
    value = Parameter(unit='degC', min=-55, max=150)
    channel = Property('channel name', StringType())

    def set_param(self, adr, value=None):
        """
        Set parameter.
        Every command starts with "[F1", and the end of line is "]".
        :param adr: second part of the command
        :param value: value to set
        :return: value converted to float
        """
        short = adr.split()[0]
        # try 3 times in case we got an asynchronous message
        for _ in range(3):
            if value is None:
                reply = self.communicate(f'[F1 {adr} ?').split()
            else:
                reply = self.communicate(f'[F1 {adr} {value}][F1 {short} ?').split()
            if reply[1] == short:
                break
        else:
            raise InternalError(f'bad reply {reply}')
        try:
            return float(reply[2])
        except ValueError:
            return reply[2]

    def get_param(self, adr):
        return self.set_param(adr)

    def read_value(self):
        return self.get_param(self.channel)

    def read_status(self):
        dt = self.parameters['value'].datatype
        if dt.min <= self.value <= dt.max:
            return IDLE, ''
        return ERROR, 'value out of range (cable unplugged?)'


class TemperatureLoopTC1(SensorTC1, Drivable):
    value = Parameter('temperature', unit='degC')
    target = Parameter('setpoint', unit='degC', min=-55, max=150)
    control = Parameter('temperature control flag', BoolType(), readonly=False)
    ramp = Parameter('ramping value', FloatRange, unit='degC/min', readonly=False)
    ramp_used = Parameter('ramping status', BoolType(), default=False, readonly=False)
    target_min = Parameter('lowest target temperature', FloatRange, unit='degC')
    target_max = Parameter('maximum target temperature', FloatRange, unit='degC')

    def read_target_min(self):
        return self.get_param('LT')

    def read_target_max(self):
        return self.get_param('MT')

    def read_status(self):
        """
        the device returns 4 symbols according to the current status. These symbols are:
        ”0” or “1” - number of unreported errors
        ”+” or “-” - stirrer is on/off
        ”+” or ”-” -  temperature control is on/off
        ”S” or “C” - current sample holder tempeerature is stable/changing
        There could be the fifth status symbol:
        ”+” or “-” or “W” - rampping is on/off/waiting
        :return: status messages
        """
        status = super().read_status()
        if status[0] == ERROR:
            return status
        reply = self.get_param('IS')  # instrument status
        if len(reply) < 5:
            self.set_param('IS', 'E+')
            reply = self.get_param('IS')  # instrument status
        self.control = reply[2] == '+'
        if reply[4] == '+':
            return BUSY, 'ramping'
        if reply[3] == 'C':
            if self.control:
                if self.ramp_used:
                    return BUSY, 'stabilizing'
                return BUSY, 'changing'
        if self.control:
            return IDLE, ''
        return WARN, 'control off'

    def write_target(self, target):
        if self.ramp_used:
            self.set_param('RR S', self.ramp)
        else:
            self.set_param('RR S', 0)
        target = self.set_param('TT S', target)
        self.set_param('TC', '+')
        self.read_status()
        return target

    def read_target(self):
        return self.get_param('TT')

    def write_control(self, control):
        if control:
            if not self.read_control():
                self.write_target(self.value)
            return True
        self.set_param('TC', '-')
        return False

    def read_ramp(self):
        return float(self.get_param('RR'))

    def write_ramp(self, ramp):
        ramp = max(0.01, abs(ramp))
        self.ramp_used = True
        ramp = self.set_param('RR S', ramp)
        if self.control:
            self.ramp = ramp
            self.write_target(self.target)
            return Done
        return ramp

    def write_ramp_used(self, value):
        if self.control:
            self.ramp_used = value
            self.write_target(self.target)
            return Done
        return value

    def stop(self):
        """stop at current value (does nothing if ramp is not used)"""
        if self.control and self.ramp_used:
            self.write_target(self.value)
