# *****************************************************************************
# 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
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Module authors:
#   Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""LakeShore Model 340"""

import time
from frappy.core import Parameter, IDLE, ERROR, IntRange
import frappy_psi.lakeshore as ls


class IO(ls.IO):
    timeout = 5  # needed for INCRV command
    model = 340
    end_of_line = '\r'  # default at SINQ. TODO: remove


class Device(ls.Device):
    ioClass = IO
    model = 340
    channels = 'ABCD'
    user_curves = (21, 61)  # the last curve is 60
    log_formats = True, True  # log Ohm / log K is supported
    _crvsav_deadline = None

    def disable_channel(self, channel):
        self.communicate(f'INSET {channel},0;*OPC?')

    def finish_curve(self, request):
        super().finish_curve(request)
        if request.loading and not self._crvsav_deadline:
            # when loading a new curve, remember for sending CRVSAV later
            self._crvsav_deadline = time.time() + 600

    def put_header(self, curve_no, name, sn, fmt, limit, coef):
        self.communicate(f'CRVHDR {curve_no},{name:15s},{sn:10s},{fmt},{limit},{coef};*OPC?')

    def doPoll(self):
        super().doPoll()
        if self._crvsav_deadline:
            # prevent flashing multiple times within short time
            if time.time() > self._crvsav_deadline:
                self._crvsav_deadline = None
                self.communicate('CRVSAV;*OPC?')

    def is_equal(self, left, right, fixeps=(1.1e-5, 1.1e-4), significant=6):
        # for whatever reason, the number of digits after decimal point for the T column is only 4
        return super().is_equal(left, right, fixeps, significant)


class Sensor340(ls.Sensor):
    model = 340
    intype = None  # arguments for the intype command

    def get_curve_type(self, calcurve):
        unit = calcurve.options.get('unit', 'Ohm')
        logformat = False
        range_limit = None
        if unit == 'Ohm':
            if calcurve.ptc:
                xlim = calcurve.xrange[1] * 0.001  # 1 mA excitation
                for rng, limit in enumerate(
                        [0, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05,
                         0.1, 0.25, 0.5, 1, 2.5, 5]):
                    if xlim <= limit:
                        break
                else:
                    rng = 13
                # we use special type here, in order to allow thermal compensation
                # <type>, <units>, <coefficient>, <excitation>, <range>
                self.intype = 0, 2, 2, 10, rng
            else:
                if calcurve.options.get('type') == 'GE':
                    self.intype = (10,)
                else:
                    self.intype = (8,)  # carbon and ruox are equivalent to cernox(8)
                logformat = True, True
        elif unit == 'V':  # diode
            self.intype = (1,) if calcurve.xscale[1] < 2.5 else (2,)
        else:  # thermocouple
            self.intype = (12,)
        return logformat, range_limit

    def install_sensor(self):
        super().install_sensor()
        if self.query(f'INSET?{self.channel}', int, int) != (1, 1):
            self.command(f'INSET {self.channel}', 1, 1)


class Loop340(ls.Loop, Sensor340):
    pass


class MainOutput340(ls.MainOutput):
    ioClass = IO
    model = 340
    output_no = Parameter(datatype=IntRange(1, 1))
    HTRST_MAP = {
        0: (IDLE, ''),
        1: (ERROR, 'Power supply over voltage'),
        2: (ERROR, 'Power supply under voltage'),
        3: (ERROR, 'Output digital-to-analog Converter error'),
        4: (ERROR, 'Current limit digital-to-analog converter error'),
        5: (ERROR, 'Open heater load'),
        6: (ERROR, 'Heater load less than 10 ohms')
    }
    _manual_output = 0.0  # TODO: check how to set this
    vmax = 50
    imax = 2
    max_currents = {4 - i: 2 ** (1 - i) for i in range(4)}
    SETPOINTLIMS = 1500.0
    sorted_factors = sorted([(fhtr * fcur ** 2, (i, h))
                             for i, fcur in max_currents.items()
                             for h, fhtr in MainOutput.heater_ranges.items()
                             ])

    def get_status(self):
        st = self.query(f'HTRST?', int)
        return self.HTRST_MAP[st]

    def configure(self):
        if self._desired_max_power is None:
            self.log.info(f'max_heater {self.writeDict} {self.max_heater}')
            self.write_max_heater(self.max_heater)
        icurrent, htr_range = self.get_best_power_idx(self._desired_max_power, 1.1)
        self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4
        self.command(f'CLIMIT {self.output_no}', self.SETPOINTLIMS, 0.0, 0.0, icurrent, htr_range)
        self._htr_range = htr_range
        if self._control_loop is None:
            mode = self.query(f'CMODE?{self.output_no}', int)
            if mode != 3:  # open loop
                self.command(f'CSET {self.output_no}', '0', 1, 0, 0)  # control off
                self.command(f'CMODE {self.output_no}', 3)  # open loop
            self.command('RANGE', self._htr_range)
            self.put_manual_power(self._manual_output)
        else:
            self.command(f'CSET {self.output_no}', self._control_loop.channel, 1, 1, 0)  # control on
            self.command(f'CMODE {self.output_no}', 1)  # pid
            self.command('RANGE', self._htr_range)
            self.put_manual_power(self._control_loop.power_offset)
        self.command(f'CDISP {self.output_no}', 1, self.resistance, 1, 0)

    def fix_heater_range(self):
        # switch heater range on, if needed
        irng = self.query('RANGE?', int)
        if irng != self._htr_range:
            if irng:
                self.log.info('output range was changed manually')
                self._htr_range = irng
            else:
                self.log.info('output was off - switch on again')
            self.command('RANGE', self._htr_range)

    def read_max_power(self):
        icurrent, htr_range = self.query(f'CLIMIT? {self.output_no}', float, float, float, int, int)[3:]
        htr_range = self.query('RANGE?', int) or htr_range
        self._htr_range = htr_range
        self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4
        return self.calc_power(100)

    def read_htr(self):
        return self.query('HTR?', float)


class AnalogOutput340(ls.AnalogOutput):
    model = 340
    output_no = Parameter(datatype=IntRange(2, 2), default=2)

    def configure(self):
        if self._control_loop is not None:
            self.put_manual_power(self.target)
        else:
            self.command('ANALOG 2', 0, 3, self._control_loop.channel)
            self.put_manual_power(self._control_loop.power_offset)

    def put_manual_power(self, value):
        if self._control_loop is None:
            self.command('ANALOG 2', 0, 2, 0, 0, 0, 0, 0, self.calc_percent(value))
        else:
            self.command(f'MOUT {self.output_no}', self.calc_percent(value))

