# *****************************************************************************
# 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>
# *****************************************************************************
"""Stanford Research Systems SIM900 Mainframe"""

import re
from frappy.core import StringIO, HasIO, Readable, \
    Parameter, FloatRange, IntRange, EnumType, \
    Property, Attached, IDLE, ERROR, WARN


def string_to_value(value):
    """
    Converting the value to float, removing the units, converting the prefix into the number.
    :param value: value
    :return: float value without units
    """
    value_with_unit = re.compile(r'(\d+)([pnumkMG]?)')
    value, pfx = value_with_unit.match(value).groups()
    pfx_dict = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, 'G': 1e9}
    if pfx in pfx_dict:
        value = round(float(value) * pfx_dict[pfx], 12)
    return float(value)


def find_idx(list_of_values, target):
    """
    Search for the nearest value and index from the given range for the given target.
    :param list_of_values: range of values
    :param target: target
    :return: closest index and closest value
    """
    target = float(target)
    cl_idx = None
    cl_value = float('inf')

    for idx, value in enumerate(list_of_values):
        if value >= target:
            diff = value - target

            if diff < cl_value:
                cl_value = value
                cl_idx = idx

    return cl_idx, cl_value


class BridgeIO(StringIO):
    """_\n is placed at the beginning of each command to distinguish
    the previous response with a possible asynchronous response from the actual value returned by the method. """

    end_of_line = '\n'
    identification = [('_\n*IDN?', r'Stanford_Research_Systems,.*')]


class Base(HasIO):
    ioClass = BridgeIO
    port = Property('modules port', IntRange(0, 15))

    def communicate(self, command):
        """
        Connection to the particular module x.
        :param command: command
        :return: return
        """
        return self.io.communicate(f'_\nconn {self.port:x},"_\n"\n{command}')

    def query(self, command):
        """converting to float"""
        return float(self.communicate(command))


class Resistance(Base, Readable):
    value = Parameter('resistance', datatype=FloatRange, unit='Ohm')
    output_offset = Parameter('resistance deviation', datatype=FloatRange, unit='Ohm', readonly=False)
    phase_hold = Parameter('phase hold', EnumType('phase hold', off=0, on=1))

    RES_RANGE = ['20mOhm', '200mOhm', '2Ohm', '20Ohm', '200Ohm', '2kOhm', '20kOhm', '200kOhm',
                 '2MOhm', '20MOhm']
    irange = Parameter('resistance range index', EnumType('resistance range index',
                                                          {name: idx for idx, name in enumerate(RES_RANGE)}),
                       readonly=False)
    range = Parameter('resistance range value', FloatRange(2e-2, 2e7), unit='Ohm', readonly=False)

    TIME_CONST = ['0.3s', '1s', '3s', '10s', '30s', '100s', '300s']
    itc = Parameter('time constant index',
                    EnumType('time const. index range',
                             {name: value for value, name in enumerate(TIME_CONST)}), readonly=False)
    tc = Parameter('time constant value', FloatRange(1e-1, 3e2), unit='s', readonly=False)

    EXCT_RANGE = ['0', '3uV', '10uV', '30uV', '100uV', '300uV', '1mV', '3mV', '10mV', '30mV']
    iexct = Parameter('excitation index',
                      EnumType('excitation index range', {name: idx for idx, name in enumerate(EXCT_RANGE, start=-1)}),
                      readonly=False)
    exct = Parameter('excitation value', FloatRange(0, 3e-2), unit='s', default=300, readonly=False)

    autorange = Parameter('autorange_on', EnumType('autorange', off=0, on=1),
                          readonly=False, default=0)

    RES_RANGE_values = [string_to_value(value) for value in RES_RANGE]
    TIME_CONST_values = [string_to_value(value) for value in TIME_CONST]
    EXCT_RANGE_values = [string_to_value(value) for value in EXCT_RANGE]

    ioClass = BridgeIO

    def doPoll(self):
        super().doPoll()
        max_res = abs(self.value)
        if self.autorange == 1:
            if max_res >= 0.9 * self.range and self.irange < 9:
                self.write_irange(self.irange + 1)
            elif max_res <= 0.3 * self.range and self.irange > 0:
                self.write_irange(self.irange - 1)

    def read_status(self):
        """
        Both the mainframe (SR900) and the module (SR921) have the same commands for the status,
        here implemented commands are made for module status, not the frame!

        :return: status type and message
        """
        esr = int(self.communicate('*esr?'))  # standart event status byte
        ovsr = int(self.communicate('ovsr?'))  # overload status
        cesr = int(self.communicate('cesr?'))  # communication error status

        if esr & (1 << 1):
            return ERROR, 'input error, cleared'
        if esr & (1 << 2):
            return ERROR, 'query error'
        if esr & (1 << 4):
            return ERROR, 'execution error'
        if esr & (1 << 5):
            return ERROR, 'command error'
        if cesr & (1 << 0):
            return ERROR, 'parity error'
        if cesr & (1 << 2):
            return ERROR, 'noise error'
        if cesr & (1 << 4):
            return ERROR, 'input overflow, cleared'
        if cesr & (1 << 3):
            return ERROR, 'hardware overflow'
        if ovsr & (1 << 0):
            return ERROR, 'output overload'
        if cesr & (1 << 7):
            return WARN, 'device clear'
        if ovsr & (1 << 2):
            return WARN, 'current saturation'
        if ovsr & (1 << 3):
            return WARN, 'under servo'
        if ovsr & (1 << 4):
            return WARN, 'over servo'
        return IDLE, ''

    def read_value(self):
        return self.query('rval?')

    def read_irange(self):
        """index of the resistance value according to the range"""
        return self.query('rang?')

    def write_irange(self, idx):
        value = int(idx)
        self.query(f'rang {value}; rang?')
        self.read_range()
        return value

    def read_range(self):
        """value of the resistance range"""
        idx = self.read_irange()
        name = self.RES_RANGE[idx]
        return string_to_value(name)

    def write_range(self, target):
        cl_idx, cl_value = find_idx(self.RES_RANGE_values, target)
        self.query(f'rang {cl_idx}; rang?')
        return cl_value

    def read_output_offset(self):
        """Output offset, can be set by user. This is the value subtracted from the measured value"""
        return self.query('rset?')

    def write_output_offset(self, output_offset):
        self.query(f'rset {output_offset};rset?')

    def read_itc(self):
        """index of the temperature constant value according to the range"""
        return self.query('tcon?')

    def write_itc(self, itc):
        self.read_itc()
        value = int(itc)
        return self.query(f'tcon {value}; tcon?')

    def read_tc(self):
        idx = self.read_itc()
        name = self.TIME_CONST[idx]
        return string_to_value(name)

    def write_tc(self, target):
        cl_idx, cl_value = find_idx(self.TIME_CONST_values, target)
        self.query(f'tcon {cl_idx};tcon?')
        return cl_value

    def read_autorange(self):
        return self.autorange

    def write_autorange(self, value):
        self.query(f'agai {value:d};agai?')
        return value

    def read_iexct(self):
        """index of the excitation value according to the range"""
        return int(self.query('exci?'))

    def write_iexct(self, iexct):
        value = int(iexct)
        return self.query(f'exci {value};exci?')

    def write_exct(self, target):
        target = float(target)
        cl_idx = None
        cl_value = float('inf')
        min_diff = float('inf')

        for idx, value in enumerate(self.EXCT_RANGE_values):
            diff = abs(value - target)

            if diff < min_diff:
                min_diff = diff
                cl_value = value
                cl_idx = idx

        self.write_iexct(cl_idx)
        return cl_value

    def read_exct(self):
        idx = int(self.read_iexct())
        name = self.EXCT_RANGE[idx + 1]
        return string_to_value(name)

    def read_phase_hold(self):
        """
        Set the phase hold mode (if on - phase is assumed to be zero).
        :return: 0 - off, 1 - on
        """
        return int(self.communicate('phld?'))

    def write_phase_hold(self, phase_hold):
        self.communicate(f'phld {phase_hold}')
        return self.read_phase_hold()


class Phase(Readable):
    resistance = Attached()
    value = Parameter('phase', FloatRange, default=0, unit='deg')

    def read_value(self):
        return self.resistance.query('phas?')


class Deviation(Readable):
    resistance = Attached()
    value = Parameter('resistance deviation', FloatRange(), unit='Ohm')

    def read_value(self):
        return self.resistance.query('rdev?')
