#-------------------------------------------------------------------------------
# Name:        module1
# Purpose:
#
# Author:      Tomas
#
# Created:     27/08/2017
# Copyright:   (c) Tomas 2017
# Licence:     <your licence>
#-------------------------------------------------------------------------------

#!/usr/bin/python

#============================ adjust path =====================================

from datetime import datetime
import struct
import sys
import os
import hartDefinition

if __name__ == '__main__':
    here = sys.path[0]
    sys.path.insert(0, os.path.join(here, '..'))

#============================ imports =========================================

import Tkinter
import traceback

from   dustUI import dustGuiLib, dustFrame
from   dustUI.dustStyle                  import dustStyle


from   SmartMeshSDK.ApiException  import CommandError, \
                                         ConnectionError, \
                                         APIError

#============================ body ============================================

class hartFrameCommand(dustFrame.dustFrame):

    def __init__(self,parentElem,guiLock, motesFrame,
                                 selectedCb,sendCb,
                                 hartDef,

                                 frameName="HART command",
                                 row=0,column=0):

        # record variables
        self.selectedCb      = selectedCb
        self.sendCb          = sendCb
        self.fieldsFrame     = None
        self.hartDef         = hartDef
        self.motesFrame      = motesFrame

        # init parent
        dustFrame.dustFrame.__init__(
            self,
            parentElem  = parentElem,
            guiLock     = guiLock,
            frameName   = frameName,
            row         = row,
            column      = column,
            scrollable = True,
        )

        with self.guiLock:
            self.commandToSend = Tkinter.StringVar()
            self.commandToSend.trace_variable('w',self._handleCommand)

        self.commandDropDownOptions = hartDef.getCommandNames()
        self.commandDropDownOptions.remove('31: Pseudo error code')

        with self.guiLock:
            self.commandDropDown = dustGuiLib.OptionMenu(
                self.container,
                self.commandToSend,
                *self.commandDropDownOptions
            )
            self._add(self.commandDropDown,0,0)


    #======================== public ==========================================
    def getCallbackId(self):
        return self.callBackId[0]
    #======================== private =========================================
    def _handleSend(self):
        broadcastCmds =[11, 21, 73]
        moteMac = self.motesFrame.getMoteMac()

        # broadcast messages cant be reliable
        if moteMac == 'Broadcast':
            moteMac = 'FF-FF-FF-FF-FF-FF-FF-FF'
            reliable = False
        else:
            reliable = True

        print('send to:', moteMac, self.selectedCmd)

        cmdNumber  = self.hartDef.getCommandId(self.selectedCmd)
        # these commandsmust
        if cmdNumber in broadcastCmds:
            reliable = False
            moteMac = 'FF-FF-FF-FF-FF-FF-FF-FF'
        cmdDataLen = 0
        cmdData    = []

        # calculate length of the request
        requstFields = self.hartDef.getRequestFieldNames(self.selectedCmd)
        for reqField in requstFields:
            cmdDataLen += self.hartDef.getRequestFieldLength(self.selectedCmd, reqField)

        # format = Status|ExtendedStatus|Cmd1(16bit)|Cmd1Length|Cmd1Data
        data = [0,0,0,cmdNumber, cmdDataLen]

        # collect data from fields, put in c['fieldValuesRaw']
        with self.guiLock:
            for c in self.guiElems:
                c['fieldValuesRaw'] = []
                for i in range(len(c['fieldNames'])):
                    if c['fieldValuesString'][i]:
                        c['fieldValuesRaw'] += [
                            c['fieldValuesString'][i].get()
                        ]
                    else:
                        c['fieldValuesRaw'] += [
                            c['fieldValuesGui'][i].get(1.0, Tkinter.END).rstrip('\n')
                        ]

        # turn c['fieldValuesRaw'] into c['fieldValues'] (also check for length)
        with self.guiLock:
            for c in self.guiElems:
                c['fieldValues'] = []
                for i in range(len(c['fieldNames'])):
                    try:
                        c['fieldValues'] += [
                            self._rawToValue(
                                c['fieldValuesRaw'][i],
                                c['fieldFormats'][i],
                                c['fieldLengths'][i],
                                c['fieldOptions'][i],
                            )
                        ]
                    except (ValueError, OverflowError):
                        c['fieldValuesGui'][i].configure(bg=dustStyle.COLOR_WARNING_FORMATTING)
                        return
                    else:
                        c['fieldValuesGui'][i].configure(bg=dustStyle.COLOR_NOERROR)

        for i in range(len(c['fieldNames'])):

            if c['fieldFormats'][i] == 'int':
                fieldData = self.int_to_bytes(c['fieldValues'][i], c['fieldLengths'][i])

            if c['fieldFormats'][i] == 'string':
                fieldData = [ord(letter) for letter in c['fieldValues'][i]]
                fieldData.extend([0] * (c['fieldLengths'][i] - len(fieldData)))

            if c['fieldFormats'][i] == 'packed':
                fieldData = [0] * c['fieldLengths'][i]
                string    = [0x20] *  (c['fieldLengths'][i]/3*4) # paddig out with spaces (HART)
                dataByte = 0

                # get array of ASCII codes
                #string = [ord(letter) for letter in c['fieldValues'][i]]
                # get array of ASCII codes
                # packed ASCII according to HART
                for k in range(len(c['fieldValues'][i])):
                    string[k] = ord(c['fieldValues'][i][k])
                    if (string[k]) >= 0x60: #small letters to caps
                        string[k] -= 0x20
                    if (string[k]) >= 0x40:
                        string[k] -= 0x40

                for j in range(len(string)/4):
                    m = j*4
                    fieldData[dataByte] = ((string[m] << 2) | (string[m+1] >> 4)) & 0xFF
                    dataByte+=1
                    fieldData[dataByte] = ((string[m+1] << 4) | (string[m+2] >> 2)) & 0xFF
                    dataByte += 1
                    fieldData[dataByte] = ((string[m+2] << 6) | string[m+3]) & 0xFF
                    dataByte += 1

            if c['fieldFormats'][i] == 'date':
                fieldData = [0,0,0]
                fieldData[0] = c['fieldValues'][i].day
                fieldData[1] = c['fieldValues'][i].month
                fieldData[2] = c['fieldValues'][i].year - 1990

            if c['fieldFormats'][i] == 'float':
                fieldData = bytearray(struct.pack('!f', c['fieldValues'][i]))

            cmdData.extend(fieldData)

        data.extend(cmdData)
        print(data)
        try:
            self.callBackId = self.connector.dn_sendRequest(
                                      macAddr=moteMac,
                                      domain='maintenance',
                                      priority='high',
                                      reliable=reliable,
                                      data=data)
            self.sendCb()
        except:
            print('not connected, cannot send')


########################################################################################
    def _handleCommand(self,name,index,mode):
        # clear the old guiElems
        self._clearGuiElems()
        if self.fieldsFrame:
            self.fieldsFrame.grid_forget()
            self.fieldsFrame = None

        c = {
            'commandName'         : None,
            'fieldNames'          : [],
            'fieldNamesGui'       : [],
            'fieldFormats'        : [],
            'fieldFormatsGui'     : [],
            'fieldLengths'        : [],
            'fieldOptions'        : [],
            'fieldValuesGui'      : [],
            'fieldValuesString'   : [],
            'fieldValuesRaw'      : [],
            'fieldValues'         : [],
            'commandORbuttonGui'  : None,
        }

        # read the command name just selected
        with self.guiLock:
            c['commandName'] = self.commandToSend.get()

        fullCmd = [elem['commandName'] for elem in self.guiElems]
        fullCmd.append(c['commandName'])
        self.selectedCmd = fullCmd

        # call the selected callback
        self.selectedCb(fullCmd)

        # add elements for fields
        try:
            c['fieldNames']   =  self.hartDef.getRequestFieldNames(fullCmd)
            c['fieldFormats'] = [
                self.hartDef.getRequestFieldFormat(fullCmd, fieldName)
                for fieldName in c['fieldNames']
            ]
            c['fieldLengths'] = [
                self.hartDef.getRequestFieldLength(fullCmd, fieldName)
                for fieldName in c['fieldNames']
            ]
            c['fieldOptions'] = [
                self.hartDef.getRequestFieldOptions(fullCmd, fieldName)
                for fieldName in c['fieldNames']
            ]
        except CommandError as err:
            traceback.print_exc()
            return

        # create a frame for all fields and add GUI elements
        with self.guiLock:
            self.fieldsFrame = Tkinter.Frame(
                self.container,
                borderwidth=10,
                bg=dustStyle.COLOR_BG,
            )

            self._add(
                self.fieldsFrame,
                row    = 2,
                column = 0,
            )

        headerColor = self._getHeaderColor()

        with self.guiLock:
            for i in range(len(c['fieldNames'])):

                # calculate width of text entry
                if c['fieldLengths'][i]:
                    if   c['fieldFormats'][i] in [hartDefinition.FieldFormats.STRING]:
                        textEntryWidth = c['fieldLengths'][i]+2
                    elif c['fieldFormats'][i] in [hartDefinition.FieldFormats.HEXDATA]:
                        textEntryWidth = c['fieldLengths'][i]*2+2
                    else:
                        textEntryWidth = dustStyle.TEXTFIELD_ENTRY_LENGTH_DEFAULT
                    autoResize = False
                else:
                    textEntryWidth = dustStyle.TEXTFIELD_ENTRY_LENGTH_DEFAULT
                    autoResize = True

                if textEntryWidth>dustStyle.TEXTFIELD_ENTRY_LENGTH_MAX:
                    textEntryWidth = dustStyle.TEXTFIELD_ENTRY_LENGTH_MAX

                # display row 0: name
                c['fieldNamesGui'] += [
                    dustGuiLib.Label(
                        self.fieldsFrame,
                        text           = c['fieldNames'][i],
                        bg             = dustStyle.COLOR_BG,
                        relief         = Tkinter.RIDGE,
                        borderwidth    = 1,
                        background     = headerColor,
                        padx           = 3,
                        pady           = 3,
                    )
                ]
                c['fieldNamesGui'][-1].grid(
                    row                = 0,
                    column             = i,
                    sticky             = Tkinter.W+Tkinter.E,
                )

                # display row 1: value
                if c['fieldOptions'][i].optionDescs or c['fieldFormats'][i]=='bool':
                    if c['fieldOptions'][i].optionDescs:
                        optionValues = c['fieldOptions'][i].optionDescs
                    else:
                        optionValues = ['True','False']
                    c['fieldValuesString'] += [Tkinter.StringVar()]
                    c['fieldValuesGui']    += [
                        dustGuiLib.OptionMenu(
                            self.fieldsFrame,
                            c['fieldValuesString'][-1],
                            *optionValues
                        )
                    ]
                else:
                    c['fieldValuesString'] += [None]
                    c['fieldValuesGui']    += [
                        dustGuiLib.Text(
                            self.fieldsFrame,
                            font       = dustStyle.FONT_BODY,
                            width      = textEntryWidth,
                            height     = 1,
                            padx       = 3,
                            pady       = 3,
                            autoResize = autoResize,
                        )
                    ]
                c['fieldValuesGui'][-1].grid(
                    row      = 1,
                    column   = i,
                    sticky   = Tkinter.W+Tkinter.E,
                )

                # display row 2: format and length
                fieldFormatsString = hartDefinition.hartDefinition.fieldFormatToString(
                    c['fieldLengths'][i],
                    c['fieldFormats'][i]
                )
                c['fieldFormatsGui']  += [
                    dustGuiLib.Label(
                        self.fieldsFrame,
                        text           = fieldFormatsString,
                        bg             = dustStyle.COLOR_BG,
                        relief         = Tkinter.RIDGE,
                        borderwidth    = 1,
                        padx           = 3,
                        pady           = 3,
                    )
                ]
                c['fieldFormatsGui'][-1].grid(
                    row      = 2,
                    column   = i,
                    sticky   = Tkinter.W+Tkinter.E,
                )

        # send button
        with self.guiLock:
            tempOptions = None
            c['commandORbuttonGui'] = dustGuiLib.Button(self.container,
                                                            command=self._handleSend,
                                                            text="send",)
            self._add(c['commandORbuttonGui'],3,0)

        # add to guiElems
        self.guiElems.append(c)

    # ======================== helpers ========================================
    def int_to_bytes(self, val, num_bytes):
        #return [((val & (0xff << pos * 8)) >> pos * 8) for pos in reversed(range(num_bytes))]
        return [((val >> pos * 8) & 0xFF) for pos in reversed(range(num_bytes))]

    def _rawToValue(self, raw, format, length, options):
        '''
        \brief convert a raw value entered by the user into a value useable by the
               connector.

        This function converts the data according to the format passed, and checks
        the resulting length.

        \param raw     The raw value, as read from the TkInter GUI element.
        \param format  The format of the resulting field.
        \param length  The length of the resulting field.
        \param options The options of the resulting field.

        \exception ValueError    The raw value can not be converted into that format.
        \exception OverflowError The raw value is not the expected size.
        \returns The converted value, in the format specified by format.
        '''
        returnVal = None

        if options.validOptions:
            # raw is one of options.optionDescs

            # return validOptions
            for i in range(len(options.optionDescs)):
                if options.optionDescs[i] == raw:
                    returnVal = options.validOptions[i]
                    break

            if returnVal == None:
                raise ValueError

        else:

            if format == 'string':

                # convert from unicode to string
                returnVal = str(raw)

                # check for length
                if length != None:
                    if len(raw) > length:
                        raise OverflowError
                    if len(raw) == 0:
                        raise ValueError

            elif format == 'bool':

                # convert
                if raw == 'True':
                    returnVal = True
                elif raw == 'False':
                    returnVal = False
                else:
                    raise ValueError

            elif (format == 'int' or format == 'ints'):

                if not raw:
                    raise ValueError

                # convert
                try:
                    returnVal = int(raw)
                except:
                    raise ValueError

                # check for length
                if length != None:
                    if returnVal >= pow(2, 8 * length):
                        raise OverflowError

            elif format == 'hex':

                # convert
                returnVal = []
                raw = ''.join(raw.split(" "))
                if len(raw) % 2 != 0:
                    raise ValueError
                try:
                    for i in range(0, len(raw), 2):
                        returnVal.append(int(raw[i:i + 2], 16))
                except ValueError:
                    raise ValueError

                # check for length
                if length != None:
                    if len(returnVal) != length:
                        raise OverflowError

            elif format == 'packed':

                # convert from unicode to string
                returnVal = str(raw)

                # check for length
                if len(raw) > (length/3)*4:
                    raise OverflowError
                if len(raw) == 0:
                    raise ValueError

            elif format == 'date':

                try:
                    date = datetime.strptime(raw, "%Y-%m-%d")
                except ValueError:
                    raise ValueError

                if (date.year < 1990) or (date.year > 2245):
                    raise ValueError

                returnVal = date

            elif format == 'float':


                try:
                    value = float(raw)
                    print(value)

                except ValueError:
                    raise ValueError

                returnVal = value;

            else:
                raise SystemError('unknown field format=' + format)

        return returnVal

#============================ sample app ======================================
# The following gets called only if you run this module as a standalone app, by
# double-clicking on this source file. This code is NOT executed when importing
# this module is a larger application
#
class exampleApp(object):

    def __init__(self):
        hartDef = hartDefinition.hartDefinition()
        self.window  = dustWindow.dustWindow("dustFrameCommand",
                                  self._closeCb)
        self.guiLock            = threading.Lock()
        # frame for active HART motes
        self.HARTmotesFrame = hartFrameMotes.hartFrameMotes(
            parentElem=self.window,
            guiLock=self.guiLock,
            hartDef=hartDef,
            frameName="HART motes",
            row=0,
            column=0
        )
        self.frame =hartFrameCommand(
            self.window,
            self.guiLock,
            self.HARTmotesFrame,
            self._selectedCb,
            self._sendCb,
            hartDef,
            row=1,
            column=0,
        )

        self.frame.show()
        self.window.mainloop()

    def _closeCb(self):
        print " _closeCb called"
    def _selectedCb(self,param):
        print " _selectedCb called with param="+str(param)
    def _sendCb(self):
        print "_sendCb called"
if __name__ == '__main__':
    import threading
    from dustUI import dustWindow
    from whart import hartFrameMotes
    exampleApp()