# *****************************************************************************
#
# 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:
#   M. Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""transducer DPM3 read out"""

import time

from frappy.core import Readable, Parameter, FloatRange, StringIO,\
     HasIO, IntRange, Done, Attached, IDLE, WARN


class DPM3IO(StringIO):
    end_of_line = '\r'
    timeout = 3
    identification = [('*1R135', '01')]


def hex2float(hexvalue, digits):
    value = int(hexvalue, 16)
    if value >= 0x800000:
        value -= 0x1000000
    return value / (10 ** digits)


def float2hex(value, digits):
    intvalue = int(round(value * 10 ** digits,0))
    if intvalue < 0:
        intvalue += 0x1000000
    return '%06X' % intvalue


class DPM3(HasIO, Readable):
    OFFSET = 0x8f
    SCALE = 0x8c

    MAGNITUDE = {'1':  1, '2':  10, '3':  100, '4':  1e3, '5':  1e4, '6':  1e5,
                 '9': -1, 'A': -10, 'B': -100, 'C': -1e3, 'D': -1e4, 'E': -1e5}

    ioClass = DPM3IO
    # when configured, the parameter drive.interlock_reason may be set to "load above limit"
    drive = Attached(mandatory=False)

    value = Parameter(datatype=FloatRange(unit='N'))
    digits = Parameter('number of digits for value', IntRange(0, 5), value=2, readonly=False)
    offset = Parameter('', FloatRange(-1e5, 1e5), readonly=False)
    scale_factor = Parameter('all shown digits in the device menu, but without dec.pt.',
                             FloatRange(-1e6, 1e6), readonly=False)
    alarm_limit = Parameter('alarm limit', FloatRange(unit='N'), readonly=False, default=2000)
    _offset_changed = 0

    def initModule(self):
        super().initModule()
        if self.drive:
            self.drive.register_interlock(self)

    def writeInitParams(self):
        for param in 'digits', 'offset', 'scale_factor':
             if param not in self.writeDict:
                 value = getattr(self, f'read_{param}')()
                 self.log.info('read %s: %r', param, value)
             else:
                 self.log.info('write %s = %r', param, self.writeDict[param])
        super().writeInitParams()

    def query(self, adr, value=None):
        if value is not None:
            if adr == self.SCALE:
                absval = abs(value)
                for nibble, mag in self.MAGNITUDE.items():
                    if 10000 <= round(value * mag, 0) <= 99999:
                        break
                else:
                    # no suitable range found
                    if absval >= 99999.5:  # overrange
                        raise ValueError('%s is out of range' % value)
                    # underrange: take lowest
                    nibble = '9' if value < 0 else '1'
                    mag = self.MAGNITUDE[nibble]
                hex_val = nibble + '%05X' % int(round(value * mag, 0))
                if hex_val[1:] == '00000':
                    raise ValueError('scale factor can not be 0', value)
            else:
                hex_val = float2hex(value, self.digits)
            cmd = '*1F3%02X%s\r' % (adr, hex_val)
        else:
            cmd = ""
        cmd = cmd + '*1G3%02X' % adr
        hexvalue = self.communicate(cmd)
        if adr == self.SCALE:
            mag = self.MAGNITUDE[hexvalue[0:1]]
            value = int(hexvalue[1:], 16)
            return value/mag
        else:
            return hex2float(hexvalue, self.digits)

    def read_value(self):
        if self._offset_changed:
            if time.time() > self._offset_changed:
                self._offset_changed = 0
            else:
                offset = self.query(self.OFFSET)
                if offset != self.offset:
                    self.log.info('offset has changed: %g, set back to %g', offset, self.offset)
                    self.query(self.OFFSET, self.offset)
                    self._offset_changed = 0
        value = float(self.communicate('*1B1'))
        if abs(value) > self.alarm_limit:
            if self.drive:
                self.drive.write_interlock_reason('load above limit')
            self.status = WARN, 'load above limit'
        else:
            self.status = IDLE, ''
        return value

    def _set_calib(self, digits, scale, offset):
        self.communicate('*1F135%02X\r*1G135' % (digits + 1))
        self.query(self.SCALE, scale * 10 ** (digits - 6))
        self.query(self.OFFSET, offset)
        self._offset_changed = time.time() + 30

    def read_digits(self):
        back_value = self.communicate('*1G135')
        return int(back_value,16) - 1

    def write_digits(self, digits):
        self._set_calib(digits, self.scale_factor, self.offset)
        return digits

    def read_offset(self):
        if self._offset_changed:
            return self.offset
        reply = self.query(self.OFFSET)
        return reply

    def write_offset(self, offset):
        self._set_calib(self.digits, self.scale_factor, offset)
        return offset

    def read_scale_factor(self):
        reply = self.query(self.SCALE)
        return float(reply) / 10 ** (self.digits - 6)

    def write_scale_factor(self, scale_factor):
        self._set_calib(self.digits, scale_factor, self.offset)
        return scale_factor
