import socket
import pandas as pd
import selectors,types
import os, pathlib
from bronkhorstControlbm31.bronkhorstServer import PORT, HOST, logdir
from bronkhorstControlbm31.bronkhorst import getParamDF
import json
import logging
import numpy as np
import time
#from datetime import datetime

homedir = pathlib.Path.home()
fulllogdir = f'{homedir}/{logdir}'
os.makedirs(fulllogdir,exist_ok=True)
logger = logging.getLogger()


def connect(host=HOST, port=PORT):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((host,port))
    return s

class MFCclient():
    def __init__(self,address, host=HOST,port=PORT, connid = socket.gethostname(), m = 1, c = 0, 
                 getMax = False):
        #dt = datetime.fromtimestamp(time.time())
        eventlogfile = f'{homedir}/{logdir}/clientEvents.log'
        logging.basicConfig(filename=eventlogfile, level = logging.INFO, format = '%(asctime)s %(levelname)-8s %(message)s',
                            datefmt = '%Y/%m/%d_%H:%M:%S')
        self.address = address
        self.host = host
        self.port = port
        self.connid = connid
        self.multi = True
        self.m = m
        self.c = c
        self.types = {'fMeasure': float, 'address':np.uint8, 'fSetpoint':float, 'Setpoint_pct':float, 'Measure_pct':float, 
                      'Valve output': float, 'Fluidset index': np.uint8,  'Control mode':np.uint8, 'Setpoint slope': int}
        self.maxCapacity = 0
        if getMax:
            self.readMaxCapacity()
        
    def strToBool(self,string):
        if string == 'True' or string == 'False':
            return string == 'True'
        return string
    def readAddresses(self):
        addressesString = self.makeSendMessage('getAddresses')
        addresses = [int(a) for a in addressesString.split()]
        self.addresses = addresses
        return addresses
    def readName(self):
        data = self.makeSendMessage( 'readName')
        return data
    def writeName(self,newname):
        data = self.makeSendMessage('writeName',newname)
        return data
    def readParam(self, name):
        data = self.makeSendMessage( 'readParam', name)
        return data
    def readParams(self,*names):
        data = self.makeSendMessage( 'readParams_names', *names)
        datadct = json.loads(data)
        return datadct
    def readFlow(self):
        data = self.makeSendMessage( 'readFlow')
        return float(data)
    def readSetpoint(self):
        data = self.makeSendMessage( 'readSetpoint')
        return float(data)

    def writeParam(self, name, value):
        data = self.makeSendMessage( 'writeParam', name, value)
        return self.strToBool(data)
    def writeSetpoint(self,value, check = False):   
        tolerance = 0.001
        if self.maxCapacity and value > self.maxCapacity:
            message = 'value given greater than maximum. Setting to maximum flow'
            logger.warning(message)
            print(message)
            value = self.maxCapacity
        string = self.makeMessage( 'writeSetpoint', value)
        try:
            data = float(self.sendMessage(string))
        except Exception as e:
            logger.exception(e)
            raise e
        success = value - tolerance < data < value + tolerance
        if not success:
            logger.warning(f'tried to write setpoint to {value}, returned setpoint: {data} .  mfc address: {self.address}, '
                           f'host: {self.host} port: {self.port}')
        if check and not success:
            raise ValueError('new setpoint doesn\'t match given value')
        if success:
            logger.info(f'mfc address {self.address} setpoint set to {data}. Host: {self.host}, port {self.port}')
        return data
    def readMaxCapacity(self):
        data = float(self.makeSendMessage( 'readMaxCapacity'))
        self.maxCapacity = data
        return data
    
    def setFlow(self,*args, **kwargs):
        '''
        alias for writeSetpoint2
        '''
        return self.writeSetpoint2(*args, **kwargs)
    def readControlMode(self):
        data = self.makeSendMessage( 'readControlMode')
        return int(data)
    def writeControlMode(self,value):
        data = int(self.makeSendMessage( 'writeControlMode',value))
        if not value == data:
            logger.warning(f'tried to write control mode to {value}, value returned: {data} to mfc address: {self.address}, '
                           f'host: {self.host}, port: {self.port}')
        return data
    def readFluidType(self):
        data = self.makeSendMessage( 'readFluidType')
        return json.loads(data)
    def writeFluidIndex(self,value):
        data = json.loads(self.makeSendMessage( 'writeFluidIndex',value))
        newFI = data['Fluidset index']
        if not value == newFI:
            logger.warning(f'tried to write fluidset index to {value}, value returned: {newFI} to mfc address: '
                           f'{self.address}, host: {self.host}, port: {self.port}')
        return data
    def readMeasure_pct(self):
        data = self.makeSendMessage('readMeasure_pct')
        return float(data)
    def readSetpoint_pct(self):
        data = self.makeSendMessage('readSetpoint_pct')
        return float(data)
    def readValve(self):
        data = self.makeSendMessage('readValve')
        return float(data)
    def readSlope(self):
        data = self.makeSendMessage('readSlope')
        return int(data)
    def writeSlope(self,value):
        data = int(self.makeSendMessage('writeSlope',value))
        if not value == data:
            logger.warning(f'tried to write slope to {value}, value returned: {data} to mfc address: {self.address}, '
                           f'host: {self.host}, port: {self.port}')
        else:
            logger.info(f'mfc address {self.address} slope set to {data}. Host: {self.host}, port {self.port}')
        return data
    def writeSP_slope(self,sp,slope):
        '''
        args: sp, slope. Write new setpoint and slope simultaneously
        '''
        data = json.loads(self.makeSendMessage('writeSP_slope',sp, slope))
        newsp = data['Setpoint']
        newSlope = data['Slope']
        success = sp-0.001 < newsp < sp+0.001 and newSlope == slope
        if not success:
            logger.warning(f'tried to set setpoint to {sp} and slope to {slope}, but returned values '
                           f'are setpoint: {newsp}, slope: {newSlope} . mfc address: {self.address}, host: {self.host}, '
                           f'port {self.port}')
        else:
            logger.info(f'setpoint set to {newsp} and slope set to {newSlope} . mfc address {self.address}, '
                        f'host {self.host}, port {self.port}')
        return data
    def calcFlow(self,flow):
        return self.m*flow + self.c
    def writeSetpoint2(self,flow,calculate = False, **kwargs):
        '''
        same as writeSetpoint, but can use calculate argument to adjust flow to linear calibration 
        based on initialised m and c values. y = m*x+c, y - flow measured by MFC, x - real measured flows (flow meter).
        You input the real flow you want, and it sets the appropriate MFC setpoint
        '''
        if calculate and flow > 0:
            flow = self.calcFlow(flow)
        return self.writeSetpoint(flow, **kwargs)
    
    def pollAll(self):
        data = self.makeSendMessage('pollAll')
        datalines = data.split('\n')
        columns = datalines[0].split(';')
        array = [[i for i in line.split(';')] for line in datalines[1:] if line]
        df = pd.DataFrame(data = array,columns=columns)
        df = df.astype(self.types)
        return df
    
    def pollAll2(self):
        data = self.makeSendMessage('readParams_allAddsPars')
        datadct = json.loads(data)
        df = pd.DataFrame.from_dict(datadct)
        df['Measure'] = df['Measure'].apply(lambda x: x*100/32000)
        df['Setpoint'] = df['Setpoint'].apply(lambda x: x*100/32000)
        df['Valve output'] = df['Valve output'].apply(lambda x: x/2**24)
        df = df.rename({'Measure':'Measure_pct', 'Setpoint':'Setpoint_pct'}, axis = 1)
        df = df.astype(self.types)
        return df
    
    def checkSetpoint(self, tolerance=0.1):
        data = self.readParams('fSetpoint', 'fMeasure')
        sp = data['fSetpoint']
        flow = data['fMeasure']
        return not sp-tolerance < flow < sp+tolerance
    
    def wait(self, tolerance = 0.1):
        while self.checkSetpoint(tolerance):
            time.sleep(1)

    def testMessage(self):
        data = self.makeSendMessage('testMessage')
        return data
    def getParamDF(self):
        return getParamDF()
    def wink(self):
        data = self.makeSendMessage('wink')
        return data
    def sendMessage(self,message):
        bytemessage = bytes(message,encoding='utf-8')
        if not self.multi:
            self.s = connect(self.host,self.port)
            self.s.sendall(bytemessage)
            data = self.s.recv(1024)
            self.s.close()
            strdata = data.decode()
            strdata = strdata.replace('!','')
        else:
            strdata = self.multiClient(bytemessage)
        print(strdata)
        return strdata
    def makeMessage(self, *args):
        sep = ';'
        string = f'{self.address}'
        for arg in args:
            string += f'{sep}{arg}'
        return string
    def makeSendMessage(self,*args):
        string = self.makeMessage(*args)
        return self.sendMessage(string)

    def multiClient(self,message):
        sel = selectors.DefaultSelector()
        server_addr = (self.host, self.port)
        print(f"Starting connection {self.connid} to {server_addr}")
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(5)
        #sock.setblocking(False)
        sock.connect_ex(server_addr)
        events = selectors.EVENT_READ | selectors.EVENT_WRITE
        data = types.SimpleNamespace(
            connid=self.connid,
            msg_total=len(message),
            recv_total=0,
            messages=[message],
            outb=b"",
        )
        sel.register(sock, events, data=data)
        try:
            while True:
                events = sel.select(timeout=1)
                if events:
                    for key, mask in events:
                        receivedMessage = self.service_connection(key, mask,sel)
                        if receivedMessage:
                            receivedMessage = receivedMessage.replace('!','')
                # Check for a socket being monitored to continue.
                if not sel.get_map():
                    break
        except KeyboardInterrupt:
            print("Caught keyboard interrupt, exiting")
        except Exception as e:
            logger.exception(f'mfc address {self.address}, host: {self.host}, port {self.port}:\n{e}')
            raise e
        finally:
            sel.close()
        return receivedMessage

    def service_connection(self,key, mask,sel):
        sock = key.fileobj
        data = key.data
        receivedMessage = b''
        strMessage = '!'
        if mask & selectors.EVENT_READ:
            while True:
                recv_data = sock.recv(1024)  # Should be ready to read
                if recv_data:
                    #print(f"Received {recv_data!r} from connection {data.connid}")
                    receivedMessage+= recv_data
                    data.recv_total += len(recv_data)
                    if receivedMessage:
                        strMessage = receivedMessage.decode()
                if not recv_data or '!' in strMessage:
                    print(f"Closing connection {data.connid}")
                    sel.unregister(sock)
                    sock.close()
                    if recv_data:
                        return strMessage
                    return
                
        if mask & selectors.EVENT_WRITE:
            if not data.outb and data.messages:
                data.outb = data.messages.pop(0)
            if data.outb:
                print(f"Sending {data.outb} to connection {data.connid}")
                sent = sock.send(data.outb)  # Should be ready to write
                data.outb = data.outb[sent:]

