# *****************************************************************************
# 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:
#   Paul M. Neves <pmneves@mit.edu>
# *****************************************************************************

import time
from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, IntRange,\
    IDLE, BUSY, WARN, ERROR, Drivable, BoolType, Attached, StructOf


class ACM1219IO(StringIO):
    """communication with ACM1219"""
    end_of_line = ('\r\n', '\r') # ('\n', '\r') ('\r\n', '\r')
    encoding = 'latin-1'  # initial reply might not be ascii for a strange reason
    identification = [('*IDN?', r'.*,ACM1219,.*')]

    def checkHWIdent(self):
        for _ in range(3):
            time.sleep(0.5)
            try:
                self.communicate('*IDN?')
                break
            except Exception:
                pass
        super().checkHWIdent()


class BothChannels(HasIO, Readable):
    """read both capacitance channels in multiplex mode"""

    # define the communication class for automatic creation of the IO module
    ioClass = ACM1219IO

    # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
    value = Parameter('Capacitance 1 and 2, and VT',
                      StructOf(C1=FloatRange(0, 21.096, unit='pF'), C2=FloatRange(0, 21.096, unit='pF'), VT=FloatRange(-1, 1000, unit='')),
                      readonly=True)
    channels_enabled = Parameter('channels on or off', BoolType(), readonly=False)
    _ch_enabled = False

    def read_value(self):
        # using the inherited HasIO.communicate method to send a command and get the reply
        natempt = 0
        maxAttempts = 5
        
        while natempt < maxAttempts:
            try:
                reply = self.communicate(f'readMUC')
                # print(reply)
                reply = reply.split(',')
                C1 = float(reply[0])
                C2 = float(reply[1])
                VT = float(reply[2])
                return {'C1': C1, 'C2': C2, 'VT': VT}
            except:
                ''
            natempt+=1
        if natempt >= maxAttempts:
            print('Max attempt reached for reading arduino.')
            return self.value
    
    def read_status(self):
        # code = self.communicate(f'readStatus') # returns tons of data
        return IDLE, ''

    def read_channels_enabled(self):
        return self._ch_enabled

    def write_channels_enabled(self, channels_enabled):
        if channels_enabled:
            self.communicate(f'setCIN 1,0,00.0,00.0,0,00.0,00.0')
            self.communicate(f'setCIN 2,0,00.0,00.0,0,00.0,00.0')
            self._ch_enabled = True
        else:
            self.communicate(f'setCIN 0,0,00.0,00.0,0,00.0,00.0')
            self._ch_enabled = False
            
        return self.read_channels_enabled()
    

class Displacement(Readable):

    # attached classes for capacitance and temperature
    cap = Attached()
    temp = Attached()

    # internal property to configure the channel
    channel = Property('the voltage channel for displacement capacitor', datatype=IntRange(1,2))

    # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
    value = Parameter('displacement', FloatRange(None, None, unit='um'), readonly=True)
    alpha290K = Parameter('capacitor constant at 290 K', FloatRange(None, None, unit='um pF'), readonly=False)
    d0 = Parameter('offset displacement', FloatRange(None, None, unit='um'), readonly=False)
    Cp = Parameter('parallel capacitance', FloatRange(None, None, unit='pF'), readonly=False)
    d0_curve = Parameter('calibration curve for offset displacement',
                      StructOf(a=FloatRange(None, None, unit='um'),
                               b=FloatRange(None, None, unit='um/K'),
                               c=FloatRange(None, None, unit='um/K^2'),
                               d=FloatRange(None, None, unit='um/K^3'),
                               e=FloatRange(None, None, unit='um/K^4'),),
                      readonly=False)

    def read_value(self):
        # get temperature and capacitance
        temp = self.temp.target
        cap = self.cap.value[f'C{self.channel}']

        # calculate displacement from temperature and capacitance
        d0_T = self.d0_curve['a'] + self.d0_curve['b']*temp + self.d0_curve['c']*temp**2 + self.d0_curve['d']*temp**3 + self.d0_curve['e']*temp**4

        disp = self.alpha290K / (cap - self.Cp) - self.d0 - d0_T

        return disp


class Force(Readable):

    # attached classes for capacitance and temperature
    cap = Attached()
    temp = Attached()

    # internal property to configure the channel
    channel = Property('the voltage channel for force capacitor', datatype=IntRange(1,2))

    # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
    value = Parameter('force', FloatRange(None, None, unit='N'), readonly=True)
    alpha290K = Parameter('capacitor constant at 290 K', FloatRange(None, None, unit='N pF'), readonly=False)
    f0 = Parameter('offset force', FloatRange(None, None, unit='N'), readonly=False)
    Cp = Parameter('parallel capacitance', FloatRange(None, None, unit='pF'), readonly=False)
    f0_curve = Parameter('calibration curve for offset force',
                      StructOf(a=FloatRange(None, None, unit='N'),
                               b=FloatRange(None, None, unit='N/K'),
                               c=FloatRange(None, None, unit='N/K^2'),
                               d=FloatRange(None, None, unit='N/K^3'),
                               e=FloatRange(None, None, unit='N/K^4'),),
                      readonly=False)

    def read_value(self):
        # get temperature and capacitance
        temp = self.temp.target
        cap = self.cap.value[f'C{self.channel}']

        # calculate force from temperature and capacitance
        alpha = self.alpha290K * (0.91 + 5e-5*temp + 9e-7*temp**2)
        f0_T = self.f0_curve['a'] + self.f0_curve['b']*temp + self.f0_curve['c']*temp**2 + self.f0_curve['d']*temp**3 + self.f0_curve['e']*temp**4
        
        force = alpha / (cap - self.Cp) - self.f0 - f0_T

        return force
    

class Stress(Readable):

    # attached classes for displacement
    force = Attached()

    # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
    value = Parameter('stress', FloatRange(None, None, unit='GPa'), readonly=True)
    area = Parameter('cross sectional area of sample in mm^2', FloatRange(None, None, unit='mm^2'), readonly=False)

    def read_value(self):
        return self.force.value / self.area / 1000


class Strain(Readable):

    # attached classes for displacement
    displacement = Attached()

    # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
    value = Parameter('strain', FloatRange(None, None, unit='m/m'), readonly=True)
    L = Parameter('length of sample in mm', FloatRange(None, None, unit='mm'), readonly=False)

    def read_value(self):
        return self.displacement.value / (1000*self.L)
    

class YoungsModulus(Readable):

    # attached classes for displacement
    stress = Attached()
    strain = Attached()

    # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype)
    value = Parameter('Young\'s modulus', FloatRange(None, None, unit='GPa'), readonly=True)

    def read_value(self):
        return self.stress.value / self.strain.value
    
