# *****************************************************************************
# 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>
# *****************************************************************************
""" CRYOMECH CP-2800 compressor
    https://github.com/Flasew/pt415/blob/master/pt415.py """

import struct
from frappy.core import BytesIO, HasIO, Writable, Parameter, BoolType, FloatRange
from frappy.rwhandler import ReadHandler
from frappy.errors import CommunicationFailedError


class IO(BytesIO):
    timeout = 5

    def getFullReply(self, request, replyheader):
        while replyheader[-1] != 13:    # CR
            replyheader += self.readBytes(1)
        return replyheader


HW_ADDR = { 'running_hours': (0x454c, 0, 60),
            'T_cpu': (0x3574, 0, 10),
            'motor_current': (0x638b, 0, 1),
            'T_inp_water': (0x0d8f, 0, 10),
            'T_inp_water_min': (0x6e58, 0, 10),
            'T_inp_water_max': (0x8a1c, 0, 10),
            'T_out_water': (0x0d8f, 1, 10),
            'T_out_water_min': (0x6e58, 1, 10),
            'T_out_water_max': (0x8a1c, 1, 10),
            'T_helium': (0x0d8f, 2, 10),
            'T_helium_min': (0x6e58, 2, 10),
            'T_helium_max': (0x8a1c, 2, 10),
            'T_oil': (0x0d8f, 3, 10),
            'T_oil_min': (0x6e58, 3, 10),
            'T_oil_max': (0x8a1c, 3, 10),
            'p_high_side': (0xaa50, 0, 10),
            'p_high_side_min': (0x5e0b, 0, 10),
            'p_high_side_max': (0x7a62, 0, 10),
            'p_high_side_avg': (0x7e90, 0, 10),
            'p_low_side': (0xaa50, 1, 10),
            'p_low_side_min': (0x5e0b, 1, 10),
            'p_low_side_max': (0x7a62, 1, 10),
            'p_low_side_avg': (0xbb94, 0, 10),
            'p_delta_avg': (0x319c, 0, 10),
            'p_high_side_bounce': (0x66fa, 0, 10)
          }

MIN_LEN = 14
ADDR = b'\x10'
STX = b'\x02'
CMD = b'\x80'
CR = b'\x0D'
DATA_WRITE = b'\x61'
DATA_READ = b'\x63'
ESC = b'\x07'
ESC_STX = b'\x07\x30'
ESC_CR = b'\x07\x31'
ESC_ESC = b'\x07\x32'
table = {STX: ESC_STX, CR: ESC_CR, ESC: ESC_ESC}


class CP2800(HasIO, Writable):

    ioClass = IO

    value = Parameter('state of compressor (on/off)', datatype=BoolType())
    target = Parameter('state of compressor (on/off)', datatype=BoolType(), readonly=False)
    running_hours = Parameter('running hours', datatype=FloatRange(unit='h'))
    T_cpu = Parameter('CPU temperature', datatype=FloatRange(unit='degC'))
    motor_current = Parameter('motor current', datatype=FloatRange(unit='A'))
    T_inp_water = Parameter('inp water temperature', datatype=FloatRange(unit='degC'))
    T_inp_water_min = Parameter('min inp water temperature', datatype=FloatRange(unit='degC'))
    T_inp_water_max = Parameter('max inp water temperature', datatype=FloatRange(unit='degC'))
    T_out_water = Parameter('out water temperature', datatype=FloatRange(unit='degC'))
    T_out_water_min = Parameter('min out water temperature', datatype=FloatRange(unit='degC'))
    T_out_water_max = Parameter('max out water temperature', datatype=FloatRange(unit='degC'))
    T_helium = Parameter('helium temperature', datatype=FloatRange(unit='degC'))
    T_helium_min = Parameter('min helium temperature', datatype=FloatRange(unit='degC'))
    T_helium_max = Parameter('max helium temperature', datatype=FloatRange(unit='degC'))
    T_oil = Parameter('oil temperature', datatype=FloatRange(unit='degC'))
    T_oil_min = Parameter('min oil temperature', datatype=FloatRange(unit='degC'))
    T_oil_max = Parameter('max oil temperature', datatype=FloatRange(unit='degC'))
    p_high_side = Parameter('high side pressure', datatype=FloatRange(unit='psi'))
    p_high_side_min = Parameter('min high side pressure', datatype=FloatRange(unit='psi'))
    p_high_side_max = Parameter('max high side pressure', datatype=FloatRange(unit='psi'))
    p_high_side_avg = Parameter('average high side pressure', datatype=FloatRange(unit='psi'))
    p_low_side = Parameter('low side pressure', datatype=FloatRange(unit='psi'))
    p_low_side_min = Parameter('min low side pressure', datatype=FloatRange(unit='psi'))
    p_low_side_max = Parameter('max low side pressure', datatype=FloatRange(unit='psi'))
    p_low_side_avg = Parameter('average low side pressure', datatype=FloatRange(unit='psi'))
    p_delta_avg = Parameter('average delta pressure', datatype=FloatRange(unit='psi'))
    p_high_side_bounce = Parameter('high side bounce pressure', datatype=FloatRange(unit='psi'))

    def checksum(self, msg):
        cksum = sum(msg)
        cksum0 = ((cksum & 0xF0) >> 4) + 0x30
        cksum1 = (cksum & 0x0F) + 0x30
        return bytes([cksum0, cksum1])


    def makePacket(self, dhash, index, val=None):
        """Make a message packet from the given parameter.
            dhash {int} - hash code representing the command.
            index {int} - index of the command.
            val {int} - if the variable is to be written into the controller, this is the value to write.
        """
        msg = ADDR + CMD
        if val is None:
            msgtype = DATA_READ
        else:
            msgtype = DATA_WRITE
        msg += msgtype
        payload = struct.pack('>HB', dhash, index)
        if val is not None:
            payload += struct.pack('>I', val)
        cksum = self.checksum(msg + payload)
        # replace ESC first to avoid recursive replaces
        msg_payload = payload.replace(ESC, ESC_ESC) \
                             .replace(CR, ESC_CR) \
                             .replace(STX, ESC_STX)
        return STX + msg + msg_payload + cksum + CR

    def deflatePacket(self, msg, reading=True):
        """Decode a returned packet. Runs checksum to check if the message is
        valid. If the original command were to read information from the controller
        this function will return the value read assume it's valid; if the
        original command were to write some value, this function will return 0 
        on success.
            msg {str} - message to be decoded
            reading {bool} - if the original message was to read some value.
        """
        if len(msg) < 6:
            raise CommunicationFailedError('deflatePacket: packet too short!')
        begin = msg[:3]
        middle = msg[3:-3]
        end = msg[-3:]
        middle = middle.replace(ESC_STX,STX) \
                       .replace(ESC_CR,CR) \
                       .replace(ESC_ESC,ESC)
        msg = begin+middle+end
        cksum = self.checksum(msg[1:-3])
        if cksum[0] != msg[-3] or cksum[1] != msg[-2]:
            raise CommunicationFailedError('bad checksum')
        if reading:
            data = struct.unpack('>I', middle[-4:])[0]
            return data
        else:
            return 0

    def write_target(self, target):
        index = 0
        dhash = 0xd501 if target else 0xc598
        msg = self.makePacket(dhash, index, target)
        reply = self.communicate(msg, 6)
        answer = self.deflatePacket(reply, reading=False)
        if answer != 0:
            return CommunicationFailedError('error when deflating packet')
        return self.read_value()

    def read_value(self):
        return self.read_motor_current() > 0

    @ReadHandler(HW_ADDR)
    def read_addressed(self, pname):
        dhash, index, scale = HW_ADDR[pname]
        msg = self.makePacket(dhash, index)
        reply = self.communicate(msg, MIN_LEN)
        data = self.deflatePacket(reply, reading=True)
        return float(data) / scale

    def doPoll(self):
        msg = self.makePacket(0xd3db, 0)    # send mm reset (from sea)
        reply = self.communicate(msg, MIN_LEN)
        answer = self.deflatePacket(reply, reading=False)
        if answer != 0:
            return CommunicationFailedError('error when deflating packet')
