# *****************************************************************************
#
# 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:
#  Andrea Plank <andrea.plank@psi.ch>
#  Anik Stark <anik.stark@psi.ch>
#
# *****************************************************************************

from frappy.core import Drivable, Parameter, Attached, FloatRange, \
    IDLE, BUSY, WARN, ERROR
from frappy.datatypes import EnumType, BoolType, StructOf, StringType
from frappy.states import Retry, Finish, status_code, HasStates
from frappy.lib.enum import Enum
from frappy.errors import ImpossibleError
from frappy.lib.mathparser import MathParser

T = Enum(off=0,  # target states
         sorbpump=1,
         condense=5,
         remove=7,
         remove_and_sorbpump=9,
         remove_and_condense=10,
         # manual=11,
         # test=12,
         )

V = Enum(T,  # value status inherits from target status
         condensing=4,
         circulating=6,
         removing=8,
         )


class Dilution(HasStates, Drivable):
    value = Parameter('current state', EnumType(V), default=0)
    target = Parameter('target state', EnumType(T), default=0)
    st = StringType()
    valve_set = StructOf(close=st, open=st, check_open=st, check_closed=st)
    condense_valves = Parameter('valve to act when condensing', valve_set)
    remove_valves = Parameter('valve to act when removing', valve_set)
    valves_after_remove = Parameter('valves to act after remove', valve_set)
    check_after_remove = Parameter('check for manual valves after remove', valve_set)
    _warn_manual_work = None

    def write_target(self, target):
        """
        if (target == Targetstates.SORBPUMP):
            if self.value == target:
                return self.target
            self.start_machine(self.sorbpump)
            self.value = Targetstates.SORBPUMP
            return self.value
        """
        self.log.info('start %s', target.name)
        if self.value == target:
            return target
        try:
            self.start_machine(getattr(self, target.name, None))
        except Exception as e:
            self.log.exception('error %s', e)
        self.log.info('started %s', target.name)
        return target

    def read_status(self):
        status = super().read_status()
        if status[0] < ERROR and self._warn_manual_work:
            try:
                self.handle_valves(**self.check_after_remove)
                self._warn_manual_work = False
            except ImpossibleError:
                return WARN, f'please close manual valves {",".join(self._valves_failed[False])}'
        return status

    def handle_valves(self, check_closed=(), check_open=(), close=(), open=()):
        """check ot set given valves
            raises ImpossibleError, when checks fails """
        self._valves_to_wait_for = {}
        self._valves_failed = {True: [], False: []}
        for flag, valves in enumerate([check_closed, check_open]):
            for vname in valves.split():
                if self.secNode.modules[vname].read_value() != flag:
                    self._valves_failed[flag].append(vname)
        for flag, valves in enumerate([close, open]):
            for vname in valves.split():
                valve = self.secNode.modules[vname]
                valve.write_target(flag)
                if valve.isBusy():
                    self._valves_to_wait_for[vname] = (valve, flag)
                elif valve.read_value() != flag:
                    self._valves_failed[flag].append(vname)

    def wait_valves(self):
        busy = False
        for vname, (valve, flag) in dict(self._valves_to_wait_for.items()):
            statuscode = valve.read_status()[0]
            if statuscode == BUSY:
                busy = True
                continue
            if valve.read_value() == flag and statuscode == IDLE:
                self._valves_to_wait_for.pop(vname)
            else:
                self._valves_failed[flag].append(vname)
        return busy

    def check_valve_result(self):
        result = []
        for flag, valves in self._valves_failed.items():
            if valves:
                result.append(f"{','.join(valves)} not {'open' if flag else 'closed'}")
                if result:
                    raise ImpossibleError(f"failed: {', '.join(result)}")


class DIL4(Dilution):
    condenseline_pressure = Attached()  # G1
    circuitshort_valve = Attached()     # V3
    still_valve = Attached()            # V6
    pumpout_valve = Attached()          # V14
    still_pressure = Attached()         # P1
    dump_pressure = Attached()          # G3
    oneK_temp = Attached()
    sorb_temp = Attached()
    still_temp = Attached()
    mix_temp = Attached()
    vti_temp = Attached()
    mix_power = Attached()

    after_remove = Parameter('target state', EnumType(sorbpump=0, condense=1, remove_finish=2),
                             readonly=False, default='remove_finish')
    sorbpumped = Parameter('sorb pump done', BoolType(), default=False, readonly=False)
    sorb_cond = Parameter('sorb condition', StringType(), default='sorb>30', readonly=False)
    sorb_pump_time = Parameter('sorb pump time', FloatRange(), default=2400, readonly=False)
    dump_target = Parameter('low dump pressure limit indicating end of condensation phase',
                            FloatRange(unit='mbar * min'), readonly=False, default=50)
    pulse_factor = Parameter('factor for calculating pump out pulse length',
                             FloatRange(unit='mbar'), readonly=False, default=20)
    end_condense_pressure = Parameter('low condense pressure indicating end of condensation phase',
                                      FloatRange(unit='mbar'), readonly=False, default=50)
    end_remove_pressure = Parameter('pressure reached before end of remove (before fore pump)',
                                    FloatRange(unit='mbar'), readonly=False, default=0.05)
    condensing_p_low = Parameter('when to start pumping dump',
                                 FloatRange(unit='mbar'), default=500, readonly=False)
    _start_time = 0

    condense_valves = {
        'close': 'v2 v3 v4 v7 v8 v10 v11 v12 v13',
        'check_closed': '',
        'check_open': '',
        'open': 'v1 v5 v9',
    }
    remove_valves = {
        'close': 'v4 v10 v13',
        'check_closed': '',
        'check_open': '',
        'open': 'v1 v2 v3 v5 v8 v9 v11 v12',
    }
    valves_after_remove = {
        'close': 'v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12',
        'check_closed': '',
        'open': '',
        'check_open': '',
    }
    check_after_remove = {
        'close': '',
        'check_closed': '',
        'open': '',
        'check_open': '',
    }

    @status_code(BUSY)
    def sorbpump(self, state):
        """ heat up to Tsorb and wait """
        if state.init:
            self.start_time = state.now
            self.vti_temp.write_target(5)
            self.handle_valves(**self.condense_valves)
            self.sorb_temp.write_target(40)  # TODO: make variable
            return Retry
        parser = MathParser(
            oneK=self.oneK_temp.value,
            still=self.still_temp.value,
            sorb=self.sorb_temp.value,
            mix=self.mix_temp.value)
        if parser.calculate(self.sorb_cond):
            self.start_time = state.now
        if state.now - self.start_time < self.sorb_pump_time:
            return Retry
        return self.sorbpump_phase2

    @status_code(BUSY)
    def sorbpump_phase2(self, state):
        if state.init:
            self.sorb_temp.write_target(1.25)
            self.start_time = state.now
            return Retry
        if state.now - self.start_time < 900:  # TODO: make variable
            return Retry
        return self.condense

    @status_code(BUSY)
    def condense(self, state):
        """ condense process """
        if state.init:
            self.vti_temp.write_target(1.25)
            # self.value = V.condensing
            self.handle_valves(**self.condense_valves)
            self.still_valve.write_target(100)
            self._start_time = state.now
            return Retry
        pdump = self.dump_pressure.value
        pcond = self.condenseline_pressure.read_value()
        if pcond < self.condensing_p_low and state.now > self._start_time + 5:
            pulse_time = 60 * self.pulse_factor / pdump
            if pulse_time > 59:
                pulse_time = 3600
            self.pumpout_valve.delay = pulse_time
            self.pumpout_valve.write_target(1)
        if pdump > self.dump_target:
            return Retry
        return self.circulate

    @status_code(BUSY)
    def circulate(self, state):
        """Zirkuliert die Mischung."""
        if state.init:
            self.handle_valves(**self.condense_valves)
        if self.wait_valves():
            return Retry
        self.check_valve_result()
        self.value = V.circulating
        return Finish

    @status_code(BUSY)
    def remove(self, state):
        self.after_remove = 'remove_finish'
        return self.remove_start(state)

    @status_code(BUSY)
    def remove_and_sorbpump(self, state):
        self.after_remove = 'sorbpump'
        return self.remove_start(state)

    @status_code(BUSY)
    def remove_and_condense(self, state):
        self.after_remove = 'condense'
        return self.remove_start(state)

    @status_code(BUSY)
    def remove_start(self, state):
        """Entfernt die Mischung."""
        if state.init:
            self.vti_temp.write_target(5)
            self.mix_power.write_target(0.02)
            self.handle_valves(**self.remove_valves)
            return Retry
        self.circuitshort_valve.write_target(1)
        return self.removing

    @status_code(BUSY)
    def removing(self, state):
        if (self.still_pressure.read_value() > 0.5 and
                self.dump_pressure.read_value() > 350):
            return Retry
        return getattr(self, self.after_remove.name)

    @status_code(BUSY)
    def remove_finish(self, state):
        if state.init:
            self.vti_temp.write_target(10)
            state.cnt_p1low = 0
        else:
            state.cnt_p1low += 1
        pstill = self.still_pressure.read_value()
        if pstill > self.end_remove_pressure or state.cnt_p1low < 3:
            return Retry
        self.handle_valves(**self.valves_after_remove)
        return self.final_status(IDLE, '')

    @status_code(BUSY)
    def close_valves_after_remove(self, state):
        if state.init:
            self.handle_valves(**self.valves_after_remove)
        self._warn_manual_work = True
        return self.final_status(WARN, 'please check manual valves')
