
import pathlib
import os
import sys
import io
import serial
import binascii
import quopri
from inspect import currentframe, getframeinfo
import struct
import time
from cview_log import *

SER_B4800 = int(4800)
SER_B9600 = int(9600)
SER_B19200 = int(19200)
SER_SPEED_DFL = SER_B19200
SER_CS8 = int(8)
SER_CS7 = int(7)
SER_SB0 = int(0)
SER_SB1 = int(1)
SER_SB2 = int(2)
SER_PARE = 'E'
SER_PARO = 'O'
SER_PARN = 'N'
SER_FLOW_NONE = int(0)
SER_FLOW_RTSCTS = int(1)
SER_FLOW_DSRDTR = int(2)
SER_FLOW_XONXOFF = int(3)
SER_WR_SLEEP = float(0.1)
CRLF = "\r\n"
LINEEND = "\n"
CWG_PROMPT = ">>"
CMD_LS = "ls"
CMD_RM = "rm"
CMD_PRINTLN = "println"
CMD_CRON_SUSPEND = "cron -"
CMD_CRON_RESUME = "cron +"
CMD_CRON_NEXT = "cron x"
CMD_HOST_ECHO_DI = "set var echo 1"
CMD_HOST_ECHO_EN = "set var echo 3"
CMD_HOST_PROMPT_DI = "set var prompt 1"
CMD_HOST_PROMPT_EN = "set var prompt 3"
CMD_VER = "ver"
CMD_DIR = "dir q"
CMD_CAT = "cat"
CMD_LCAT = "lcat"
CMD_UPLOAD = "upload"
CMD_FSIZE = "fsize"
CMD_UPLOAD_Q = "upload q"
CMD_UPLOAD_QB = "upload q b"
CMD_SCR_LOAD = "scr load"
CMD_SCR_RUN = "scr run"
CMD_SCR_STAT = "scr stat"
CMD_SCR_UNLOAD = "scr unload"
CMD_LI_POWER_EN = "mdio +ul +li "
CMD_LI_POWER_DI = "mdio -li -ul "
CMD_CRON_EN = "cron +"
CMD_CRON_DI = "cron -"
CMD_CTRL_D = "\x04"
CWG_IOBUF_BYTES = int(2048)
CWG_RES_BYTES = int(24)
CWG_IO_RTO_SEC = int(1)
CWG_IO_WTO_SEC = int(1)
CWG_IO_ITO_SEC = float(0.01)
FSIZE_RET_BYTES = int(12)
CWG_UTEST_DAT = "utest.dat"
CWG_UTEST_UP = "utest.up"
CWG_UTEST_DOWN = "utest.dn"
CWG_UTEST_CSV = "utest.csv"
CWG_UTEST_BIN = "utest.bin"
TEST_VER = 0x001
TEST_PRINTLN= 0x002
TEST_UPLOAD = 0x004
TEST_DOWNLOAD = 0x008
TEST_LI_POWER = 0x010
TEST_RD_DIR = 0x020
TEST_FGET_REC = 0x040
TEST_MGET_REC= 0x080
TEST_STREAM= 0x100

class CwgIO():
    def __init__(self):
        self.io_ctx = serial.Serial()
        self.set_flow(SER_FLOW_NONE)
        self.set_speed(SER_SPEED_DFL)
        cwg_log.debug('create cwg_io instance.')

    def show(self):
        print("    device: ",self.io_ctx.port);
        print("     speed: ",self.io_ctx.baudrate);
        print("     csize: ",self.io_ctx.bytesize);
        print("    parity: ",self.io_ctx.parity);
        print("     stopb: ",self.io_ctx.stopbits);
        print("    rtscts: ",self.io_ctx.rtscts);
        print("   xonxoff: ",self.io_ctx.xonxoff);
        print("    dsrdtr: ",self.io_ctx.dsrdtr);
        print("   rto_sec: ",self.io_ctx.timeout)
        print("   wto_sec: ",self.io_ctx.write_timeout)
        print("   ito_sec: ",self.io_ctx.inter_byte_timeout)
        print(" connected: ",self.io_ctx.is_open)
        return

    def set_device(self,device):
        self.io_ctx.port = device
        return

    def set_speed(self,speed):
        retval = 0
        if speed==SER_B9600 or speed==SER_B19200 or speed==SER_B4800:
            self.io_ctx.baudrate=speed
        else:
            cwg_log.error( "unsupported speed:"+speed )
            retval=-1
        return retval

    def set_csize(self,csize):
        retval = 0
        if csize==SER_CS6:
            self.io_ctx.bytesize=SIXBITS
        elif csize==SER_CS7:
            self.io_ctx.bytesize=SEVENBITS
        elif csize==SER_CS8:
            self.io_ctx.bytesize=EIGHTBITS
        else:
            cwg_log.error("unsupported csize:"+csize)
            retval=-1
        return retval

    def set_parity(self,parity):
        retval = 0
        if parity==SER_PARN:
            self.io_ctx.parity=PARITY_NONE
        elif parity==SER_PARE:
            self.io_ctx.parity=PARITY_EVEN
        elif par==SER_PARO:
            self.io_ctx.parity=PARITY_ODD
        else:
            cwg_log.error("unsupported parity:"+parity)
            retval=-1
        return

    def set_flow(self,flow):
        retval = 0
        if flow==SER_FLOW_NONE:
            self.io_ctx.rtscts=False
            self.io_ctx.xonxoff=False
            self.io_ctx.dsrdtr=False
        elif flow==SER_FLOW_RTSCTS:
            self.io_ctx.rtscts=True
            self.io_ctx.xonxoff=False
            self.io_ctx.dsrdtr=False
        elif flow==SER_FLOW_DSRDTR:
            self.io_ctx.rtscts=False
            self.io_ctx.xonxoff=False
            self.io_ctx.dsrdtr=True
        elif flow==SER_FLOW_XONXOFF:
            self.io_ctx.rtscts=False
            self.io_ctx.xonxoff=True
            self.io_ctx.dsrdtr=False
        else:
            cwg_log.error("unsupported flow:"+parity)
            retval=-1
        return retval

    def set_rto(self,to_sec):
        self.io_ctx.timeout=to_sec
        return

    def set_wto(self,to_sec):
        self.io_ctx.write_timeout=to_sec
        return

    def set_ito(self,to_sec):
        self.io_ctx.inter_byte_timeout=to_sec
        return

    def connect(self):
        retval = 0
        try:
            self.io_ctx.close()
            self.io_ctx.open()
        except Exception as e:
            cwg_log.exception("connect error")
            retval = -1
        return retval

    def disconnect(self):
        retval = 0
        try:
            self.io_ctx.close()
        except Exception as e:
            cwg_log.exception("disconnect error")
            retval = -1
        return

    def is_connected(self):
        return self.io_ctx.is_open


    def rx_waiting(self):
        return self.io_ctx.in_waiting

    def read(self,rlen,rt_sec=None):
        if not rt_sec is None:
            save_to = self.io_ctx.timeout
            self.io_ctx.timeout=rt_sec

        cwg_log.debug("read rlen["+str(rlen)+"] rt["+str(rt_sec)+"]")
        retval = self.io_ctx.read(rlen)
        # frameinfo = getframeinfo(currentframe())
        # cwg_log.debug(frameinfo.filename+str(frameinfo.lineno))
        br=len(retval)
        # len(retval.decode(encoding="ascii"))

        if not rt_sec is None:
            self.io_ctx.timeout=save_to
        return retval,br

    def write(self,src,wt_sec=None,it_sec=None):
        if not wt_sec is None:
            save_wt = self.io_ctx.write_timeout
            self.io_ctx.write_timeout=wt_sec
        if not it_sec is None:
            save_it = self.io_ctx.inter_byte_timeout
            self.io_ctx.inter_byte_timeout=it_sec

        cwg_log.debug("w>> "+str(src)+" wt["+str(wt_sec)+"] it["+str(it_sec)+"]")
        retval = self.io_ctx.write(src)
        self.io_ctx.flush()

        # write doesn't complete w/o delay (or read seems to work)
        # write_timeout and inter_byte_timeout don't seem to help
        if SER_WR_SLEEP > 0.0 :
            time.sleep(SER_WR_SLEEP)
        if not wt_sec is None:
            self.io_ctx.write_timeout=save_wt
        if not it_sec is None:
            self.io_ctx.inter_byte_timeout=save_it
        return retval

    def read_until(self,term,ilen,rt_sec=None):
        if not rt_sec is None:
            save_rt = self.io_ctx.timeout
            self.io_ctx.timeout = rt_sec

        retval = self.io_ctx.read_until(term,ilen)
        br = len(retval)

        if not rt_sec is None:
            self.io_ctx.timeout = save_rt

        return retval,br

    def write_read(self,src,r_len,term,rt_sec=None,wt_sec=None,it_sec=None):
        bw = self.write(src,wt_sec,it_sec)
        cwg_log.debug("wr>> ["+str(src)+"/"+str(bw)+"]")
        vmsg,br = self.read(r_len,rt_sec)
        # cwg_log.debug("wr<< [",vmsg.decode(),"]\r\n len[",br,"]")
        return vmsg,br

    def drain_rx(self,rt_sec=0):
        retval = 0
        self.io_ctx.reset_input_buffer()
        while True:
            iobuf,br = self.read(1,1)
            cwg_log.debug("drain br["+str(br)+"] io["+str(iobuf)+"]")
            if br==0:
                break;
        return retval

    def drain_tx(self,rt_sec=0):
        retval = 0
        self.io_ctx.reset_output_buffer()
        return retval

    def set_echo(self, enabled):
        retval = -1
        if enabled :
            cbytes=self.cmd_bytes(CMD_HOST_ECHO_EN+CRLF)
        else:
            cbytes=self.cmd_bytes(CMD_HOST_ECHO_DI+CRLF)

        rval,br = self.write_read(cbytes,CWG_RES_BYTES,None,CWG_IO_RTO_SEC,None,None)
        cwg_log.debug("se<< ["+str(rval)+"/"+str(br)+"]")
        return retval

    def set_prompt(self, enable):
        retval = 0
        if enable :
            cbytes=self.cmd_bytes(CMD_HOST_PROMPT_EN+CRLF)
        else:
            cbytes=self.cmd_bytes(CMD_HOST_PROMPT_DI+CRLF)
        try:
            rval,br = self.write_read(cbytes,CWG_RES_BYTES,None,CWG_IO_RTO_SEC,None,None)
            cwg_log.debug("se<< ["+str(rval)+"/"+str(br)+"]")
        except Exception as e:
            retval = -1

        return retval

    def upload_file(self,dest,src,binary):
        retval = 0
        try:
            src_file = open(src,'r+b')

            self.set_echo(False)
            self.set_prompt(False)
            if binary:
                slen = os.path.getsize(src)
                cmd = self.cmd_bytes(CMD_UPLOAD+" q b:"+str(slen)+" T5 "+dest.decode()+CRLF)
            else:
                cmd = self.cmd_bytes(CMD_UPLOAD+" q T5 "+dest.decode()+CRLF)

            self.write(cmd)

            read_len=0
            while True:
                try:
                    iobuf=src_file.read(CWG_IOBUF_BYTES)
                    rb = len(iobuf.decode())
                    if rb>0:
                        read_len += rb
                        self.write(iobuf,CWG_IO_RTO_SEC,CWG_IO_ITO_SEC)
                    else:
                        break
                except Exception as e:
                    cwg_log.exception("read error")

            if binary:
                term_s = "BIN_EOT\r\n"
                eot,br = self.read_until(term_s.encode(),9,CWG_IO_RTO_SEC)

            src_file.close()
        except Exception as e:
            cwg_log.exception("upload_file error")
            retval=-1

        return retval

    def download_file(self,dest,src,binary):
        retval = 0
        try:
            dest_file = open(dest,'w+b')

            self.set_echo(False)
            self.set_prompt(False)
            cmd_s = CMD_FSIZE+" "+src.decode()+CRLF
            cmd = self.cmd_bytes(cmd_s)
            fsz_b,br = self.write_read(cmd,FSIZE_RET_BYTES,None,CWG_IO_RTO_SEC,None,None)

            if br>0:
                cmd = self.cmd_bytes(CMD_CAT+" rb: "+src.decode()+CRLF)
                self.write(cmd,CWG_IO_RTO_SEC,CWG_IO_ITO_SEC)
            else:
                cwg_log.error("fsize err - rval["+fsz_b.decode()+"] br["+br+"]\r\n")

            read_len=0
            pending=int(fsz_b.decode())
            retries=3
            while pending > 0 and retries > 0 :
                try:
                    iobuf,rb=self.read(CWG_IOBUF_BYTES,CWG_IO_RTO_SEC)
                    if rb>0:
                        cwg_log.debug("rb["+str(rb)+"]...")
                        pending -= rb
                        dest_file.write(iobuf)
                        retries=3
                    else:
                        retries-=1
                except Exception as e:
                    cwg_log.exception("download read error")

            if binary:
                term_s = "BIN_EOT\r\n"
                eot,br = self.read_until(term_s.encode(),len(term_s),CWG_IO_RTO_SEC)

            dest_file.close()
        except Exception as e:
            cwg_log.exception("download_file error")
            retval=-1
        return retval

    # rtype: "o", "z"
    # range: "Ln", "Sn Cm"
    # ofmt: "bcrx"
    # dest: file
    # src: source file
    def fget_records(self,dest,rtype,range,ofmt,src=None):
        retval=0
        cmd_s = (CMD_LCAT+" "+rtype+" "+range+" out:h,"+ofmt)
        if not src is None:
            cmd_s += " src:"+src
        cmd_s += CRLF
        cmd = self.cmd_bytes(cmd_s)
        cwg_log.debug("fget_records cmd["+str(cmd)+"]")

        try:
            dest_file = open(dest,"w+b")
            self.set_echo(False)
            self.set_prompt(False)

            self.write(cmd,CWG_IO_RTO_SEC,CWG_IO_ITO_SEC)
            retries = 3
            read_total=0
            while retries > 0 :
                try:
                    iobuf,rb=self.read(CWG_IOBUF_BYTES,CWG_IO_RTO_SEC)
                    if rb>0:
                        read_total += rb
                        cwg_log.debug("rb["+str(rb)+"]...")
                        dest_file.write(iobuf)
                        retries=3
                    else:
                        retries-=1

                    retval = read_total
                except Exception as e:
                    cwg_log.exception("fget read error")
                    retval=-1
            dest_file.close()
        except Exception as e:
            cwg_log.exception("fget error")
            retval=-1

        return retval

    # rtype: "o", "z"
    # range: "Ln", "Sn Cm"
    # ofmt: "bcrx"
    # src : source file
    def mget_records(self,rtype,range,ofmt,src=None):
        retval=0
        cmd_s = (CMD_LCAT+" "+str(rtype)+" "+str(range)+" out:h,"+ofmt)
        if not src is None:
            cmd_s += " src:"+src
        cmd_s += CRLF
        cmd = self.cmd_bytes(cmd_s)
        cwg_log.debug("fget_records cmd["+str(cmd)+"]")
        dest_mem = bytearray()

        try:
            self.set_echo(False)
            self.set_prompt(False)

            self.write(cmd,CWG_IO_RTO_SEC,CWG_IO_ITO_SEC)
            retries = 3
            read_total=0
            while retries > 0 :
                try:
                    iobuf,rb=self.read(CWG_IOBUF_BYTES,CWG_IO_RTO_SEC)
                    if rb>0:
                        read_total += rb
                        cwg_log.debug("rb["+str(rb)+"]...")
                        dest_mem.extend(iobuf)
                        retries=3
                    else:
                        retries-=1

                    retval = read_total
                except Exception as e:
                    cwg_log.exception("mget read error")
                    retval=-1

        except Exception as e:
            cwg_log.exception("mget_records error")
            retval=-1

        return dest_mem,retval

    def licor_power(self,enable):
        retval = 0
        if enable:
            cbytes=self.cmd_bytes(CMD_LI_POWER_EN+CRLF)
        else:
            cbytes=self.cmd_bytes(CMD_LI_POWER_DI+CRLF)
        try:
            rval,br = self.write_read(cbytes,CWG_RES_BYTES,None,CWG_IO_RTO_SEC,None,None)
            cwg_log.debug("lipow<< ["+str(rval)+"/"+str(br)+"]")
        except Exception as e:
            cwg_log.exception("licor_power error:")
            retval = -1
        return retval

    def set_cron(self,enable):
        retval = 0
        if enable:
            cbytes=self.cmd_bytes(CMD_CRON_EN+CRLF)
        else:
            cbytes=self.cmd_bytes(CMD_CRON_DI+CRLF)
        try:
            rval,br = self.write_read(cbytes,CWG_RES_BYTES,None,CWG_IO_RTO_SEC,None,None)
            cwg_log.debug("cron<< ["+str(rval)+"/"+str(br)+"]")
        except Exception as e:
            cwg_log.exception("set_cron error")
            retval = -1
        return retval

    def read_dir(self):
        cbytes=self.cmd_bytes(CMD_DIR+CRLF)
        try:
            self.write(cbytes,CWG_IO_RTO_SEC,CWG_IO_ITO_SEC)
            dest_mem=bytearray()
            retries = 3
            read_total=0
            while retries > 0 :
                try:
                    iobuf,rb=self.read(CWG_IOBUF_BYTES,CWG_IO_RTO_SEC)
                    if rb>0:
                        read_total += rb
                        cwg_log.debug("rb["+str(rb)+"]...")
                        dest_mem.extend(iobuf)
                        retries=3
                    else:
                        retries-=1

                except Exception as e:
                    cwg_log.exception("read_dir read error")

        except Exception as e:
            cwg_log.exception("read_dir error:")

        cwg_log.debug("read_dir<< ["+str(dest_mem)+"/"+str(read_total)+"]")

        return dest_mem,read_total

    def send_esc(self,count=1):
        cbytes = self.cmd_bytes(CMD_CTRL_D)
        for x in range(count):
            self.write_read(cbytes,1,None,1,None,None)
        self.write_read(CRLF.encode(),1,None,1,None,None)
        return

    def start_streaming(self, period_sec, log_stream=False):
        if log_stream:
            log_opt = "l*"
        else:
            log_opt = "ln"

        try:
            cmd_s = ("mmeas tm:air,fo,p"+'{:.1f}'.format(period_sec)+",t0,ln,h:c"+CRLF)
            cbytes = self.cmd_bytes(cmd_s)
            self.write(cbytes)
        except Exception as e:
            cwg_log.exception("start_streaming error")
        return

    def send_cmd(self, cmd):
        retval = 0
        cbytes=self.cmd_bytes(cmd+CRLF)
        try:
            rval,br = self.write_read(cbytes,CWG_RES_BYTES,None,CWG_IO_RTO_SEC,None,None)
            cwg_log.debug("%s<< [%s/%d]", cmd, str(rval), br)
        except Exception as e:
            cwg_log.exception("send_cmd error:")
            retval = -1
        return retval

    def cmd_bytes(self,cmdstr):
        rval = bytearray(cmdstr,encoding='ascii')
        return rval


    @staticmethod
    def unit_test(opts):
        retval=0
        try:
            ser = CwgIO()
            ser.set_device(opts["device"])
            ser.set_speed(opts["speed"])
            ser.show()
            cwg_log.debug("instance:"+str(ser.io_ctx.get_settings()))
            ser.connect()
            flags = opts["flags"]
            if ser.is_connected():

                ser.send_esc(3)
                ser.licor_power(False)
                time.sleep(2)
                ser.drain_rx()
                ser.licor_power(True)
                time.sleep(2)

                if flags&TEST_VER != 0:
                    cwg_log.debug("### Test VER ###")
                    cbytes=ser.cmd_bytes(CMD_VER+CRLF)
                    vmsg,br = ser.write_read(cbytes,160,None,CWG_IO_RTO_SEC,None,None)
                    cwg_log.debug("ver<< ["+str(vmsg)+"]\r\n len["+str(br)+"]")

                if flags&TEST_PRINTLN != 0:
                    cwg_log.debug("### Test PRINTLN ###")
                    msg ="'this is a message'"
                    cbytes=ser.cmd_bytes(CMD_PRINTLN+" "+msg+CRLF)
                    vmsg,br = ser.write_read(cbytes,len(msg),None,CWG_IO_RTO_SEC,None,None)
                    cwg_log.debug("println<< ["+str(vmsg)+"]\r\n len["+str(br)+"]")

                if flags&TEST_UPLOAD != 0 or flags&TEST_DOWNLOAD!=0:
                    cwg_log.debug("### gen test data ###")
                    test_file = open(CWG_UTEST_DAT,"w+b");
                    tbytes = bytearray(127)
                    i=0
                    for x in tbytes[0:128:1]:
                        tbytes[i] = i
                        i+=1
                    test_file.write(tbytes)
                    test_file.close();

                if flags&TEST_UPLOAD != 0 or flags&TEST_DOWNLOAD!=0:
                    cwg_log.debug("### Test UPLOAD ###")
                    ser.upload_file(CWG_UTEST_UP,CWG_UTEST_DAT,True)

                if flags&TEST_DOWNLOAD!=0:
                    cwg_log.debug("### Test DOWNLOAD ###")
                    ser.download_file(CWG_UTEST_DOWN,CWG_UTEST_UP,True)

                if flags&TEST_FGET_REC!=0:
                    cwg_log.debug("### Test FGET_RECORDS (csv) ###")
                    ser.fget_records(CWG_UTEST_CSV,"O","L3","c","$CBIN")
                    cwg_log.debug("### Test FGET_RECORDS (raw) ###")
                    ser.fget_records(CWG_UTEST_BIN,"O","L3","r","$CBIN")

                if flags&TEST_MGET_REC!=0:
                    cwg_log.debug("### Test MGET_RECORDS ###")
                    rbytes,sz = ser.mget_records("O","L3","r","$CBIN")
                    if sz>0:
                        cwg_log.debug("rbytes["+str(sz)+"]")
                        cwg_log.debug("{"+str(binascii.b2a_hex(rbytes))+"}")

                if flags&TEST_RD_DIR!=0:
                    cwg_log.debug("### Test READ_DIR ###")
                    dbytes,br = ser.read_dir()
                    cwg_log.debug("dir:\r\n"+str(dbytes)+"\r\nlen["+str(br)+"]\r\n")

                if flags&TEST_STREAM!=0:
                    cwg_log.debug("### Test START_STREAM ###")
                    ser.send_esc(3)
                    ser.drain_rx()
                    ser.licor_power(True)
                    ser.start_streaming(1.0,False)
                    for line in range(4):
                        buf,br = ser.read_until(CRLF.encode(),256,5)
                        cwg_log.debug("line["+str(buf)+"] len["+str(br)+"]")
                    ser.send_esc(3)
                    ser.drain_rx()
                    ser.licor_power(False)

                if flags&TEST_LI_POWER!=0:
                    cwg_log.debug("### Test LI_POWER ###")
                    ser.licor_power(True)
                    time.sleep(3)
                    ser.licor_power(False)

                ser.licor_power(False)
            else:
                cwg_log.error("connect failed")

        except Exception as e:
            cwg_log.exception("unit_test error")
            retval=-1


        return retval

    @staticmethod
    def parse_test_options(argv):

        opts = {"device":None,"speed":19200,"flags":0}
        aflag = None

        for i, arg in enumerate(argv):

            # if aflag set, assign this arg
            # to the associated entry
            if not aflag is None:
                if aflag == "dev":
                    opts["device"]=arg
                    aflag=None
                if aflag == "speed":
                    opts["speed"]=int(arg)
                    aflag=None
                if aflag=="flags":
                    opts["flags"]=int(arg,16)
                    aflag=None

            if arg.startswith("--dev="):
                opts["device"]=arg.replace("--dev=","")
            if arg.startswith("-d"):
                x = arg.replace("-d","")
                if len(x) == 0:
                    aflag="dev"
                else:
                    opts["device"]=x

            if arg.startswith("--speed="):
                opts["speed"]=int(arg.replace("--speed=",""))
            if arg.startswith("-s"):
                x = arg.replace("-s","")
                if len(x) == 0:
                    aflag="speed"
                else:
                    opts["speed"]=int(x)

            if arg.startswith("--flags="):
                opts["flags"]=int(arg.replace("--flags=",""),16)
            if arg.startswith("-f"):
                x = arg.replace("-f","")
                if len(x) == 0:
                    aflag="flags"
                else:
                    opts["flags"]=int(x,16)
        return opts


if __name__ == '__main__':
#    cwg_log.debug(f"Arguments count: {len(sys.argv)}")
    opts = CwgIO.parse_test_options(sys.argv)
    cwg_log.debug("opts["+str(opts)+"]")
    test = CwgIO.unit_test(opts)
    exit(test)
