# *****************************************************************************
# 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:
#   Anik Stark <anik.stark@psi.ch>
#   Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************

"""Bronkhorst flow or pressure regulators. Communication via ProPar protocol.

write command:  :LnAd01PrTpData\r\n
read command:   :LnAd04CopyPrTp\r\n    (Ln = 06)
    answer:     :LnAd02CopyData\r\n

    Ln: number of bytes (hex digits pairs) following
    Ad: node address (3...120, always a reply if message is sent to node address 128)
    Copy: values just to be copied by the reply (first and third digit < 8)
        recommended practice: Use PrTp for Copy
    Pr: Process number (<80, manual page 24pp)
    Tp: Type + parameter number. Type: 00 byte, 20 int, 40 long/float, 60 string
        for strings either 00 (for nul terminated) or the max. number of chars
        has to be appended to the type
    Data: length depending on type.

    read command for direct communication:  :06800401210120

Send values on a scale from 0-32000 (0-100%).
"""

from frappy.core import StringIO, HasIO, Readable, Writable, Drivable, Parameter, Property, \
    FloatRange, BoolType, EnumType, IntRange, IDLE, BUSY
from frappy.errors import CommunicationFailedError


class IO(StringIO):
    end_of_line = '\r\n'    # hex: 0D0A
    addr = 128
    identification = [(f':07{addr:02X}047163716300', f':10{addr:02X}02716300.*')]  # serial number
    default_settings = {'baudrate': 38400}


def intpar(process, parameter):
    return '06', f'{process:02X}{parameter|0x20:02X}'

def longpar(process, parameter):
    return '08', f'{process:02X}{parameter|0x40:04X}'

MEASURE = intpar(1, 0)
SETPOINT = intpar(1, 1)
RAMP = intpar(1, 2)
CONTROL = longpar(114, 1)


class Sensor(HasIO, Readable):

    ioClass = IO

    value = Parameter('pressure', FloatRange())
    scale = Property('scale factor', FloatRange(), default=1)
    addr = Property('node adress', IntRange(0, 255), default=128)

    def get_par(self, length, param, scale):
        reply = self.communicate(f':{length}{self.addr:02X}04{param}{param}')
        if reply[:11] != f':{length}{self.addr:02X}02{param}':
            return CommunicationFailedError(f'bad reply: {reply}')
        val = int(reply[11:15], 16) / 32000 * scale
        return val

    def read_value(self):
        return self.get_par(*MEASURE, self.scale)


class Controller(Sensor, Writable):

    def set_par(self, length, param, scale, value):
        reply = self.communicate(f':{length}{self.addr:02X}01{param}{round(value * 32000 / scale):04X}')
        if reply[:11] != f':04{self.addr:02X}000005':
            raise CommunicationFailedError(f'bad reply: {reply}')
        return self.get_par(length, param, scale)

    def read_target(self):
        return self.get_par(*SETPOINT, self.scale)

    def write_target(self, value):
        return self.set_par(*SETPOINT, self.scale, value)


class HasRamp(Drivable):

    setpoint = Parameter('running setpoint', FloatRange())
    ramp_enable = Parameter('enable ramp mode', BoolType())
    ramp = Parameter('slope of ramp', FloatRange(1e-6, unit='mbar/min'))
    tolerance = Property('tolerance for target vs. running setpoint', FloatRange(), default=1)

    def read_target(self):
    # overwrite Controller.read_target() as setpoint is running
        return self.read_target

    def write_target(self, target):
        super().write_target(target)
        self.status = BUSY, 'ramping'

    def read_setpoint(self):
        return super().read_target()

    def read_ramp(self):
        if abs(self.read_setpoint() - self.target) < self.tolerance:
            self.status = IDLE, ''

    def write_ramp(self, ramp):
        if self.ramp_enable:
            time = min(self.scale / ramp, 3000)
            return self.set_par(*RAMP, (60 / 0.1), time)

    def write_ramp_enable(self, flag):
        if flag:
            self.write_ramp(self.ramp)
        else:
            self.set_par(*RAMP, (60 / 0.1), 0)


class HasControlMode():

    control_active = Parameter('control mode active', BoolType())
    control = Property('control mode', EnumType(manual=4, loop=11))
    output = Parameter('valve output', FloatRange(), readonly=False)

    def write_control(self, value):
        if self.control_active:
            val = self.control.get(value, 4)
            return self.set_par(*CONTROL, 1, val)

    def write_output(self, value):
        scale = (2**24 - 1) / 100
        self.set_par(*CONTROL, scale, value)


class ControllerRamp(HasRamp, Controller):
    pass


class ControllerControlMode(HasControlMode, Controller):
    pass


class ControllerRampControlMode(HasRamp, HasControlMode, Controller):
    pass
