import usb.core
import usb.util
from usb import USBError
import csafe_cmd
import datetime
import time
import sys

C2_VENDOR_ID = 0x17a4
MIN_FRAME_GAP = .050 #in seconds
INTERFACE = 0

def find():
    ergs = usb.core.find(find_all=True, idVendor=C2_VENDOR_ID)
    if ergs is None:
        raise ValueError('Ergs not found')
    return ergs


class pyrow(object):

    def __init__(self, erg):
        """
        Configures usb connection and sets erg value
        """

        if sys.platform != 'win32':
            try:
                #Check to see if driver is attached to kernel (linux)
                if erg.is_kernel_driver_active(INTERFACE):
                    erg.detach_kernel_driver(INTERFACE)
                else:
                    print "DEBUG: usb kernel driver not on " + sys.platform
            except:
                print "EXCEPTION"

        #Claim interface (Needs Testing To See If Necessary)
        usb.util.claim_interface(erg, INTERFACE)

        #Linux throws error, reason unknown
        try:
            erg.set_configuration() #required to configure USB connection
            #Ubuntu Linux returns 'usb.core.USBError: Resource busy' but rest of code still works
        except Exception as e:
            if not isinstance(e, USBError):
                raise e
        self.erg = erg

        configuration = erg[0]
        iface = configuration[(0, 0)]
        self.inEndpoint = iface[0].bEndpointAddress
        self.outEndpoint = iface[1].bEndpointAddress

        self.__lastsend = datetime.datetime.now()

    @classmethod
    def __checkvalue(cls, value, label, minimum, maximum):
        """
        Checks that value is an integer and within the specified range
        """

        if type(value) is not int:
            raise TypeError(label)
        if  not minimum <= value <= maximum:
            raise ValueError(label + " outside of range")
        return True

    def get_monitor(self, forceplot=False):
        """
        Returns values from the monitor that relate to the current workout,
        optionally returns force plot data and stroke state
        """

        command = ['CSAFE_PM_GET_WORKTIME', 'CSAFE_PM_GET_WORKDISTANCE', 'CSAFE_GETCADENCE_CMD',
                   'CSAFE_GETPOWER_CMD', 'CSAFE_GETCALORIES_CMD', 'CSAFE_GETHRCUR_CMD']

        if forceplot:
            command.extend(['CSAFE_PM_GET_FORCEPLOTDATA', 32, 'CSAFE_PM_GET_STROKESTATE'])
        results = self.send(command)

        monitor = {}
        monitor['time'] = (results['CSAFE_PM_GET_WORKTIME'][0] + \
            results['CSAFE_PM_GET_WORKTIME'][1])/100.

        monitor['distance'] = (results['CSAFE_PM_GET_WORKDISTANCE'][0] + \
            results['CSAFE_PM_GET_WORKDISTANCE'][1])/10.

        monitor['spm'] = results['CSAFE_GETCADENCE_CMD'][0]
        #Rowing machine always returns power as Watts
        monitor['power'] = results['CSAFE_GETPOWER_CMD'][0]
        if monitor['power']:
            monitor['pace'] = ((2.8 / results['CSAFE_GETPOWER_CMD'][0]) ** (1./3)) * 500
            monitor['calhr'] = results['CSAFE_GETPOWER_CMD'][0]  * (4.0 * 0.8604) + 300.
        else:
            monitor['pace'], monitor['calhr'] = 0, 0
        monitor['calories'] = results['CSAFE_GETCALORIES_CMD'][0]
        monitor['heartrate'] = results['CSAFE_GETHRCUR_CMD'][0]

        if forceplot:
            #get amount of returned data in bytes
            datapoints = results['CSAFE_PM_GET_FORCEPLOTDATA'][0] /2
            monitor['forceplot'] = results['CSAFE_PM_GET_FORCEPLOTDATA'][1:(datapoints+1)]
            monitor['strokestate'] = results['CSAFE_PM_GET_STROKESTATE'][0]

        monitor['status'] = results['CSAFE_GETSTATUS_CMD'][0] & 0xF

        return monitor

    def get_force_plot(self):
        """
        Returns force plot data and stroke state
        """

        command = ['CSAFE_PM_GET_FORCEPLOTDATA', 32, 'CSAFE_PM_GET_STROKESTATE']
        results = self.send(command)

        forceplot = {}
        datapoints = results['CSAFE_PM_GET_FORCEPLOTDATA'][0] / 2
        forceplot['forceplot'] = results['CSAFE_PM_GET_FORCEPLOTDATA'][1:(datapoints+1)]
        forceplot['strokestate'] = results['CSAFE_PM_GET_STROKESTATE'][0]

        forceplot['status'] = results['CSAFE_GETSTATUS_CMD'][0] & 0xF

        return forceplot


    def get_workout(self):
        """
        Returns overall workout data
        """

        command = ['CSAFE_GETID_CMD', 'CSAFE_PM_GET_WORKOUTTYPE', 'CSAFE_PM_GET_WORKOUTSTATE',
                   'CSAFE_PM_GET_INTERVALTYPE', 'CSAFE_PM_GET_WORKOUTINTERVALCOUNT']
        results = self.send(command)

        workoutdata = {}
        workoutdata['userid'] = results['CSAFE_GETID_CMD'][0]
        workoutdata['type'] = results['CSAFE_PM_GET_WORKOUTTYPE'][0]
        workoutdata['state'] = results['CSAFE_PM_GET_WORKOUTSTATE'][0]
        workoutdata['inttype'] = results['CSAFE_PM_GET_INTERVALTYPE'][0]
        workoutdata['intcount'] = results['CSAFE_PM_GET_WORKOUTINTERVALCOUNT'][0]

        workoutdata['status'] = results['CSAFE_GETSTATUS_CMD'][0] & 0xF

        return workoutdata

    def get_erg(self):
        """
        Returns all erg data that is not related to the workout
        """

        command = ['CSAFE_GETVERSION_CMD', 'CSAFE_GETSERIAL_CMD', 'CSAFE_GETCAPS_CMD', 0x00]
        results = self.send(command)

        ergdata = {}
        #Get data from csafe get version command
        ergdata['mfgid'] = results['CSAFE_GETVERSION_CMD'][0]
        ergdata['cid'] = results['CSAFE_GETVERSION_CMD'][1]
        ergdata['model'] = results['CSAFE_GETVERSION_CMD'][2]
        ergdata['hwversion'] = results['CSAFE_GETVERSION_CMD'][3]
        ergdata['swversion'] = results['CSAFE_GETVERSION_CMD'][4]
        #Get data from csafe get serial command
        ergdata['serial'] = results['CSAFE_GETSERIAL_CMD'][0]
        #Get data from csafe get capabilities command
        ergdata['maxrx'] = results['CSAFE_GETCAPS_CMD'][0]
        ergdata['maxtx'] = results['CSAFE_GETCAPS_CMD'][1]
        ergdata['mininterframe'] = results['CSAFE_GETCAPS_CMD'][2]

        ergdata['status'] = results['CSAFE_GETSTATUS_CMD'][0] & 0xF

        return ergdata

    def get_status(self):
        """
        Returns the status of the erg
        """

        command = ['CSAFE_GETSTATUS_CMD', ]
        results = self.send(command)

        status = {}
        status['status'] = results['CSAFE_GETSTATUS_CMD'][0] & 0xF

        return status


    def set_clock(self):
        """
        Sets the erg clock to the computers current time and date
        """

        now = datetime.datetime.now() #Get current date and time

        command = ['CSAFE_SETTIME_CMD', now.hour, now.minute, now.second]
        command.extend(['CSAFE_SETDATE_CMD', (now.year-1900), now.month, now.day])

        self.send(command)

    def set_workout(self, program=None, workout_time=None, distance=None, split=None,
                    pace=None, calpace=None, powerpace=None):
        """
        If machine is in the ready state, function will set the
        workout and display the start workout screen
        """

        self.send(['CSAFE_RESET_CMD'])
        command = []

        #Set Workout Goal
        if program != None:
            self.__checkvalue(program, "Program", 0, 15)
        elif workout_time != None:
            if len(workout_time) == 1:
                #if only seconds in workout_time then pad minutes
                workout_time.insert(0, 0)
            if len(workout_time) == 2:
                #if no hours in workout_time then pad hours
                workout_time.insert(0, 0) #if no hours in workout_time then pad hours
            self.__checkvalue(workout_time[0], "Time Hours", 0, 9)
            self.__checkvalue(workout_time[1], "Time Minutes", 0, 59)
            self.__checkvalue(workout_time[2], "Time Seconds", 0, 59)

            if workout_time[0] == 0 and workout_time[1] == 0 and workout_time[2] < 20:
                #checks if workout is < 20 seconds
                raise ValueError("Workout too short")

            command.extend(['CSAFE_SETTWORK_CMD', workout_time[0],
                            workout_time[1], workout_time[2]])

        elif distance != None:
            self.__checkvalue(distance, "Distance", 100, 50000)
            command.extend(['CSAFE_SETHORIZONTAL_CMD', distance, 36]) #36 = meters

        #Set Split
        if split != None:
            if workout_time != None and program == None:
                split = int(split*100)
                #total workout workout_time (1 sec)
                time_raw = workout_time[0]*3600+workout_time[1]*60+workout_time[2]
                #split workout_time that will occur 30 workout_times (.01 sec)
                minsplit = int(time_raw/30*100+0.5)
                self.__checkvalue(split, "Split Time", max(2000, minsplit), time_raw*100)
                command.extend(['CSAFE_PM_SET_SPLITDURATION', 0, split])
            elif distance != None and program == None:
                minsplit = int(distance/30+0.5) #split distance that will occur 30 workout_times (m)
                self.__checkvalue(split, "Split distance", max(100, minsplit), distance)
                command.extend(['CSAFE_PM_SET_SPLITDURATION', 128, split])
            else:
                raise ValueError("Cannot set split for current goal")


        #Set Pace
        if pace != None:
            powerpace = int(round(2.8 / ((pace / 500.) ** 3)))
        elif calpace != None:
            powerpace = int(round((calpace - 300.)/(4.0 * 0.8604)))
        if powerpace != None:
            command.extend(['CSAFE_SETPOWER_CMD', powerpace, 88]) #88 = watts

        if program == None:
            program = 0

        command.extend(['CSAFE_SETPROGRAM_CMD', program, 0, 'CSAFE_GOINUSE_CMD'])

        self.send(command)

    def send(self, message):
        """
        Converts and sends message to erg; receives, converts, and returns ergs response
        """

        #Checks that enough time has passed since the last message was sent,
        #if not program sleeps till time has passed
        now = datetime.datetime.now()
        delta = now - self.__lastsend
        deltaraw = delta.seconds + delta.microseconds/1000000.
        if deltaraw < MIN_FRAME_GAP:
            time.sleep(MIN_FRAME_GAP - deltaraw)

        #convert message to byte array
        csafe = csafe_cmd.write(message)
        #sends message to erg and records length of message
        length = self.erg.write(self.outEndpoint, csafe, timeout=2000)
        #records time when message was sent
        self.__lastsend = datetime.datetime.now()

        response = []
        while not response:
            try:
                #recieves byte array from erg
                transmission = self.erg.read(self.inEndpoint, length, timeout=2000)
                response = csafe_cmd.read(transmission)
            except Exception as e:
                raise e
                #Replace with error or let error trigger?
                #No message was recieved back from erg
                # return []

        #convers byte array to response dictionary
        return response