import sys
import io
from collections import defaultdict
import struct
from time import sleep
import queue
import threading

import serial
from serial import SerialException

RUN_LABELS = ('Time left', 'Temp 1', 'Temp 2', 'Off Goal', 'Temp Change', 'Duty cycle (/30)', 'Heating', 'Cycle', 'Total time', 'Goal temp')

MSG_RUN_STATUS = 1
MSG_CONFIG = 2
MSG_STATUS = 3
MSG_LENGTHS = {MSG_RUN_STATUS: 20, MSG_CONFIG: 9, MSG_STATUS: 5}

STATE_START = 1
STATE_ACTIVE = 2
STATE_READY = 3
STATE_BOOT = 4
STATE_INIT = 5
STATE_DISCONNECTED = 127 # can't connect to serial

HB_CYCLE = 30

class RunStatus:
    __slots__ = ('countdown', 't1', 't2', 'dg', 'dt', 'part', 'state', 'cycle', 'time', 'goal')
    def __init__(self, message):
        (self.t1,
         self.t2,
         self.countdown,
         self.part,
         self.cycle,
         self.state,
         self.dg,
         self.dt,
         self.time,
         self.goal,
        ) = struct.unpack('=BBLBB?bbLB', message)

    def __str__(self):
        return "\t".join(
            map(str,
                (self.countdown,
                 self.t1,
                 self.t2,
                 self.dg,
                 self.dt,
                 self.part,
                 "On" if self.state else "Off",
                 self.state,
                 self.cycle,
                 self.time,
                 self.goal,
             )
            ))

class OvenConfig:
    __slots__ = ('temp', 'time')
    def __init__(self, message):
        (self.time,
        self.temp) = struct.unpack('=LB', message)

class OvenStatus:
    __slots__ = ('status',)
    def __init__(self, message):
        self.status = message[0]

def check_connection(fun):
    def inner(self, *args, **kwargs):
        if self.state == "connected":
            try:
                fun(self, *args, **kwargs)
            except SerialException:
                self.disconnect()
            # workaround for bug in pyserial
            # http://sourceforge.net/p/pyserial/patches/37/
            except TypeError as e:
                self.disconnect()
    return inner

class Client(threading.Thread):
    """ Client class for hotbox serial connection """
    parsers = {
        MSG_STATUS: OvenStatus,
        MSG_RUN_STATUS: RunStatus,
        MSG_CONFIG: OvenConfig,
    }

    def __init__(self):
        super().__init__()
        self.state = 'disconnected'
        self.msg_queue = {MSG_STATUS: queue.Queue(), 
                          MSG_CONFIG: queue.Queue(), 
                          MSG_RUN_STATUS: queue.Queue(),
                      }

    def connect(self, port):
        try:
            self.conn = serial.Serial(port, 9600, timeout=0.05)
            # empty buffer
            while len(self.conn.read(1)) > 0:
                pass

            self.state = 'connected'

            sleep(0.01)
            self.oven_query_config()

            sleep(0.2)
            self.oven_status()

        except SerialException:
            self.disconnect()
        # workaround for bug in pyserial
        # http://sourceforge.net/p/pyserial/patches/37/
        except TypeError as e:
            self.disconnect()
        finally:
            self.start_message = 0

    def run(self):
        self.running = 1

        parsed_length = 0
        mtype = 0
        msg_length = 0

        
        while self.running:

            # Don't do anything if disconnected
            if (self.state == 'disconnected'):
                sleep(0.1)
                continue

            try:
                c = self.conn.read(1)
            except SerialException:
                self.disconnect()
                continue
            # workaround for bug in pyserial
            # http://sourceforge.net/p/pyserial/patches/37/
            except TypeError as e:
                self.disconnect()
                continue


            # wait for message
            if not c:
                continue

            # this is the message type byte
            if parsed_length == 3:
                parsed_length += 1
                if c[0] == 0:
                    continue
                mtype = c[0]
                msg_length = MSG_LENGTHS[mtype]
                buffer = bytes()
                continue

            if parsed_length < 3:
                # Abort if not a null byte
                if c[0]:
                    parsed_length = 0
                    continue
                # otherwise increment parsed length
                parsed_length += 1
                continue

            # in any other case this is a data byte
            parsed_length += 1
            buffer += c
            if parsed_length == msg_length:
                data = self.parsers[mtype](buffer)
                self.msg_queue[mtype].put(data)
                parsed_length = 0
                mtype = 0
                msg_length = 0

    @check_connection
    def oven_configure(self, ctime, temp):
        self.conn.write(b'c'+struct.pack('=LB', ctime, temp))

    @check_connection
    def oven_start(self):
        self.conn.write(b's')

    @check_connection
    def oven_stop(self):
        self.conn.write(b't')

    @check_connection
    def oven_status(self):
        self.conn.write(b'r')

    @check_connection
    def oven_query_config(self):
        self.conn.write(b'q')

    def disconnect(self):
        self.state = 'disconnected'
        self.msg_queue[MSG_STATUS].put(OvenStatus((STATE_DISCONNECTED,)))


[((41, 12, 41, 49), 'struct.unpack', 'struct.unpack', ({(41, 26, 41, 39): '"""=BBLBB?bbLB"""', (41, 41, 41, 48): 'message'}, {}), "('=BBLBB?bbLB', message)", False, 'import struct\n'), ((64, 21, 64, 50), 'struct.unpack', 'struct.unpack', ({(64, 35, 64, 40): '"""=LB"""', (64, 42, 64, 49): 'message'}, {}), "('=LB', message)", False, 'import struct\n'), ((95, 38, 95, 51), 'queue.Queue', 'queue.Queue', ({}, {}), '()', False, 'import queue\n'), ((96, 38, 96, 51), 'queue.Queue', 'queue.Queue', ({}, {}), '()', False, 'import queue\n'), ((97, 42, 97, 55), 'queue.Queue', 'queue.Queue', ({}, {}), '()', False, 'import queue\n'), ((102, 24, 102, 63), 'serial.Serial', 'serial.Serial', (), '', False, 'import serial\n'), ((109, 12, 109, 23), 'time.sleep', 'sleep', ({(109, 18, 109, 22): '(0.01)'}, {}), '(0.01)', False, 'from time import sleep\n'), ((112, 12, 112, 22), 'time.sleep', 'sleep', ({(112, 18, 112, 21): '(0.2)'}, {}), '(0.2)', False, 'from time import sleep\n'), ((136, 16, 136, 26), 'time.sleep', 'sleep', ({(136, 22, 136, 25): '(0.1)'}, {}), '(0.1)', False, 'from time import sleep\n'), ((186, 29, 186, 60), 'struct.pack', 'struct.pack', ({(186, 41, 186, 46): '"""=LB"""', (186, 48, 186, 53): 'ctime', (186, 55, 186, 59): 'temp'}, {}), "('=LB', ctime, temp)", False, 'import struct\n')]