##
# @package dbc
# @version 1.9
# @brief   Read DBC files
# @author  Henry Daitx (edited by JB)
# @date    Jun 2017
# @copyright Racelogic Ltd., 2017
#

'''Changes from Henry's original code:
    - Added support for 32 and 64 Bit IEEE Floats
    - fixed signing bug (changed '+' to '-')
    - Added a modified version of Value Decode Function used in VDMS2 (fixed from v1.8)
    - Added a basic Flash save and Reload Option (needs improvement to account for memory limits)
    - Added File-names find function for convenience
    - Tested NamedTuple and Dictionary types to Determine best option
    - Added support to ignore VECTOR__INDEPENDENT_SIG_MSGs as they lack a CAN_ID
    - Set all units to lowercase to ensure compatibility
    - Added Exception handling for incorrectly formatted files
    - Added extended info function to retrieve signal description if available
    - Removed the unknown intel start bit modification from dbc load as I think its taken care of
      in the value_decode function (although perhaps should modify at dbc import to speed up...)
'''


# import gc
import os
from ucollections import namedtuple
# Named tuple has same performance speeds as Dictionary, but takes up less space
# Because it is a tuple, its immutable and cannot be changed once created
from micropython import const

# time = None

#---Signal Types---#
INT         = const(0)
FLOAT_32    = const(1)
FLOAT_64    = const(2)
#------------------#

Signal = namedtuple('Signal', (     #Must be declared globally to allow loading of stored config
        'name',
        'mux',
        'start',
        'len',
        'intel',
        'signed',
        'scale',
        'offset',
        'min',
        'max',
        'units',
        'type',
        'group',
        'id',
    ))

class DBC_Error(Exception):
    pass

def read_dbc(fn):
    # if time: time.restart()
    # global Signal
    new_signal = Signal #Signal quicker if imported locally to stack
    No_ID = False
    signals = {}
    with open(fn) as f:
        for l in f:
            l = l.strip()
            if not l:
                continue
            elif l[:4] == 'BO_ ':
                try:
                    # From Tex's code:
                    #   Format for line is BO_ a b: c d
                    #   where a = Identifier
                    #   b = Group
                    #   c = DLC
                    #   d = not used by us
                    #   Eliminate the colon and split by whitespace
                    l = ''.join(l.split(':')).split()
                    # print(l)
                    group_id = int(l[1]) #hex()?
                    group_name = l[2]
                    group_dlc = int(l[3])
                    if group_dlc > 8:
                        group_dlc = 8
                    No_ID = True if group_name == 'VECTOR__INDEPENDENT_SIG_MSG' else False
                except (IndexError, ValueError):
                    raise DBC_Error("Unable to parse group: {}".format(l))
            elif (l[:4] == 'SG_ ') and not (No_ID):
                try:
                # From Tex's code:
                #   Format for line is SG_ a M :b|c@d+ (e,f) [g|h] i j
                #   where a = Name
                #   M is optional. M = Multiplexor mx = multiplexed signal where
                #     x = the multiplexor group
                #   b = start bit
                #   c = length
                #   d = Motorola / Intel
                #   + = signed / (-)unsigned
                #   e = scale
                #   f = offset
                #   g = minimum
                #   h = maximum
                #   i = units
                #   j = not used by us
                    l = l.split(':')
                    # Work first with the part before the colon
                    t = l[0].split()
                    signal_name = t[1]
                    signal_mux = None
                    if len(t) > 2:
                        if t[2] == 'M':
                            signal_mux = 'multiplexor'
                        else:
                            signal_mux = int(t[2][1:])
                    # Now work with the part after the colon
                    l = l[1]
                    signal_start = int(l[:l.index('|')])
                    l = l[l.index('|') + 1:]
                    signal_len = int(l[:l.index('@')])
                    l = l[l.index('@') + 1:]
                    signal_Intel = l[0] == '1'
                    # if not signal_Intel:
                    #     # No idea why this adjustment is required for Motorola
                    #     for i in range(signal_len - 1):
                    #         if not signal_start & 7:
                    #             signal_start += 16
                    #         signal_start -= 1
                    l = l[1:]
                    signal_signed = l[0] == '-'
                    l = l[l.index('(') + 1:]
                    signal_scale = float(l[:l.index(',')])
                    l = l[l.index(',') + 1:]
                    signal_offset = float(l[:l.index(')')])
                    l = l[l.index('[') + 1:]
                    signal_min = float(l[:l.index('|')])
                    l = l[l.index('|') + 1:]
                    signal_max = float(l[:l.index(']')])
                    l = l[l.index(']') + 1:]
                    signal_units = l.split()[0].replace('\"', '').lower()
                except (ValueError, IndexError):
                    raise DBC_Error("Unable to parse signal: {}".format(l))

                sig_num = 1
                while signal_name in signals.keys(): #keys must be unique
                    signal_name = signal_name + '_{}'.format(sig_num) if (sig_num is 1) else signal_name[:-1] + str(sig_num)
                    sig_num += 1

                # print(signal_name)
                signals[signal_name] = new_signal(
                    signal_name,
                    signal_mux,
                    signal_start,
                    signal_len,
                    signal_Intel,
                    signal_signed,
                    signal_scale,
                    signal_offset,
                    signal_min,
                    signal_max,
                    signal_units,
                    -1, # signal_type specified later
                    group_name,
                    group_id,
                )
            elif l[:13] == 'SIG_VALTYPE_ ':
                try:
                    l = l.split(':')
                    signal_type = int(l[1].strip()[:1])
                    if signal_type not in range(3): #See Float Types at top
                        signal_type = -1
                    l = l[0].split()
                    signal_id = hex(int(l[1]))
                    signal_name = l[2]
                except (ValueError, IndexError):
                    raise DBC_Error("Unable to parse Signal ValType: {}".format(l))

                # Quickest way is to just replace tuple if we find a type rather than search through file for
                # signal type when creating the original signal tuple (maybe run GC after all signals processed)
                if (signal_name in signals.keys()) and (signals[signal_name].id == signal_id):
                    # print("Match for {} on ID {}".format(signal_name, signal_id))
                    signals[signal_name] = new_signal(
                            signals[signal_name].name,
                            signals[signal_name].mux,
                            signals[signal_name].start,
                            signals[signal_name].len,
                            signals[signal_name].intel,
                            signals[signal_name].signed,
                            signals[signal_name].scale,
                            signals[signal_name].offset,
                            signals[signal_name].min,
                            signals[signal_name].max,
                            signals[signal_name].units,
                            signal_type,
                            signals[signal_name].group,
                            signals[signal_name].id,
                            )
    # if time: print("Read_DBC_time: {:0.3f}s".format(time.read()/1000000))
    #gc.collect()?
    return signals

def get_val(frame, msg_info):
    from ustruct import pack, unpack
    if msg_info.mux:
        raise ValueError('multiplexor signal not implemented')

    start = msg_info.start
    length = msg_info.len
    # psigned = True if msg_info.type == 'psigned' else False       #Old RL Format?
    double = msg_info.type == FLOAT_64
    single = msg_info.type == FLOAT_32
    # scale = msg_info.scale
    # offset = msg_info.offset
    # signed = msg_info.signed
    # minimum = msg_info.min
    # maximum = msg_info.max

    if (double and length != 64) or (single and length != 32):
        raise DBC_Error('Illegal {} width ({}): {}'.format(
            'FLOAT64' if double else 'FLOAT32', msg_info.name,
            length
        ))

    frame = list(frame)
    # Default: Motorola (big endian) order. Internal processing is always
    # done in Intel (little endian) order.
    # NOTE:
    # Broadcast start bit is on LEAST significant byte
    if not msg_info.intel:
        frame.reverse()
        start = ((len(frame) - (start >> 3) - 1) * 8) + (start & 7)
    # Crop data to right length
    try:
        frame = frame[(start >> 3):(((start + length) >> 3) + bool((start + length) & 7))]
    except IndexError:
        raise DBC_Error("Index Error Decoding Value - Invalid length")
    start &= 7

    # Get raw value of all bits
    res = sum((v << (i * 8)) for i, v in enumerate(frame))

    # Shift to right place and mask
    res >>= start
    res &= (1 << length) - 1

    if double:
        res = unpack('d', pack('Q', res))[0]
    elif single:
        res = unpack('f', pack('L', res))[0]
    # elif psigned and ((1 << (length - 1)) & res):
    #     res = -(res & ~(1 << (length - 1)))
    elif msg_info.signed and ((1 << (length - 1)) & res):
        res = -((1 << length) - res)
    return res * msg_info.scale + msg_info.offset

def store_CAN_config(file_name, config, directory='/flash/CAN_configs'):
    '''Stores CAN config in a text file that can be quickly loaded
       in again later (similar to pickling).
       Note: Configs with identical names are overwritten'''
    try:
        os.mkdir(directory)
    except OSError:
        pass #Directory already created
    if len(os.listdir(directory)) > 10:
        os.unlink("{}/{}".format(directory,os.listdir(directory)[0])) #removes oldest?

    try:
        with open("{}/{}".format(directory, file_name), "w") as f:
            f.write(repr(config))
            print("New Config Stored")
            return 1
    except Exception as e:
        print("Unable to store config: {}".format(e))
        return 0

def load_stored_CAN_config(file_name, directory='/flash/CAN_configs'):
    try:
        with open('{}/{}'.format(directory, file_name), 'r') as f:
            file_string = f.read()

        loaded_config = eval(file_string, {'Signal':Signal})
    except OSError: #config or directory does not exist
        return 0
    except Exception as e:
        print("Unable to load '{}' config: {}".format(file_name, e))
        return 0

    return loaded_config

def get_dbc_file_names(directory='/sd', include_extension=True):
    dbc_files = [x for x in os.listdir(directory) if x[-4:].lower() == '.dbc']
    return dbc_files if include_extension else [file[:-4] for file in dbc_files]

def get_extended_signal_info(signal, file_path):
    try:
        with open(file_path) as file:
            for line in file:
                if line.startswith('CM_') and (str(signal.id) in line) and (signal.name in line):
                    return line
    except OSError:
        raise DBC_Error("DBC File not Available")
    return ""

if __name__ == "__main__":
    print("DBC Functions Under Test")
    # from vts import Chrono; time = Chrono()

    # x=0
    # gc.collect()
    # start = gc.mem_free()
    # b = {   'name': 1,
    #         'mux': 2,
    #         'start': 3,
    #         'len': 4,
    #         'intel': 5,
    #         'signed': 6,
    #         'scale': 7,
    #         'offset': 8,
    #         'min': 9,
    #         'max': 10,
    #         'units': 11,
    #         'type': 12,
    #         'id': 13}
    # size = gc.mem_free()-start
    # time.restart()
    # x = b['name']
    # print("Dict: {}, size={}".format(time.read(), size))
    # #=----------------=#
    # gc.collect()
    # start = gc.mem_free()
    # a = Signal(1,2,3,4,5,6,7,8,9,10,11,12,13)
    # size = gc.mem_free()-start
    # time.restart()
    # x = a.name
    # print("Named_tuple: speed={}, size={}".format(time.read(), size))



    # c = read_dbc('/sd/McLaren GT3 720s.dbc')
    # cd = {ckey: 0 for ckey in c.keys()}
    # d = read_dbc('/sd/3_Target_T1.dbc')

    # with open('/flash/CAN_configs/test_save', 'w') as w:
    #     w.write(repr(d))

    # bob = load_stored_CAN_config('test_save')
    # with open('/sd/test_save', 'r') as f:
    #     file_string = f.read()

    # loaded_config = eval(file_string)

    # can = [123,34,5,56,70,8,89,91]

    # params = {'Spd_sv': 0,
    # 'LaneR_tg1': 0,
    # 'LaRtg1_tg1': 0,
    # 'LnRtg1_tg1': 0,
    # 'Angle_tg1': 0,
    # 'LatRef_tg1': 0,
    # 'LngRef_tg1': 0,
    # 'LatRsv_tg1': 0,
    # 'LngRsv_tg1': 0,
    # 'Range_tg1': 0}

    # for runs in range(100):
    #     for i in cd.keys():
    #         time.restart()
    #         get_val(can, c[i])
    #         cd[i] += (time.read()/1000)

    # for key, val in cd.items():
    #     print("{} --- Decode time: {:0.4f}ms".format(key, (val/100)))

    # try:
    #     os.mkdir("/flash/CAN_configs")
    # except OSError:
    #     os.unlink("/flash/CAN_configs")
    #     # os.mkdir("/flash/CAN_configs")
    #     print('it exists!')

    # for i in range(10):
    #     with open("/flash/CAN_configs/{}.txt".format(i), 'w') as f:
    #         f.write("Test"*100)

    # print(os.listdir('/flash/CAN_configs/'))

