# Creates a firmware image ready for upload with the USB bootloader.
# This tool adds the firmware length and CRC-32 code.

import os
import sys
from typing import NamedTuple
import ihex_loader
from crc import Crc32, CrcCalculator

import argparse

class MCU_Config(NamedTuple):
    name: str
    flash_address: int
    flash_size: int
    bootloader_size: int
    signature: int
    page_size: int
    vector_offset: int
    crc_bits: int
    rows_per_block: int

samg55 = MCU_Config('SAMG55', 0x00400000, 0x00080000, 0, 0x1302FD67, 0x200, 0x400, 16, 4)

USB_BOOTLOADER_SIZE = 0x4000 # 16kB for USB bootloader, app starts after this

def load_bin(filename):
    with open(filename, 'rb') as f:
        return [bb for bb in f.read()]

def generate_lut_entry(lut_index):
    crc_tab_entry = lut_index
    for i in range(8, 0, -1):
        if (crc_tab_entry & 0x01):
            crc_tab_entry = (crc_tab_entry >> 1) ^ 0xEDB88320

        else:
            crc_tab_entry = crc_tab_entry >> 1
    return(crc_tab_entry)

def calc_end_crc(bfr, length):
    crc32 = 0xffffffff

    for i in range(0,length):
        data_byte = bfr[i]

        idx = (data_byte ^ (crc32 & 0x000000FF))

        # work out the CRC
        # the CRC function is the IEEE CRC32 function with a polynomial
        # of 0xEDB88320, implemented using a lookup table
        crc32 = (crc32 >> 8)

        crc32 = crc32 ^ generate_lut_entry(idx)

    return crc32

def record_16_crc(crc, ch):
    crcPoly = 0x8005

    m = (crc << 8) | ch

    for n in range(0,8):
        m = m << 1
        if (m & 0x1000000):
            m ^= (crcPoly << 8)

    return (m >> 8) & 0xffff

def get_crc16(frame):
    Frame_CRC_val = 0
    for j in range(2, len(frame)):
        Frame_CRC_val = record_16_crc(Frame_CRC_val, frame[j])
    Frame_CRC_val = record_16_crc(Frame_CRC_val, 0)
    Frame_CRC_val = record_16_crc(Frame_CRC_val, 0)

    return Frame_CRC_val

def get_crc32(frame):
    return calc_end_crc(frame[2:], len(frame)-2)

def check_is_blank_or_ff(input_bytes):
    for bb in input_bytes:
        if(bb != 0 and bb != 0xFF):
            return False

    return True

def create_fw_image_for_usb_bootloader(input_file, output_file):
    # Check if input_file is recognizable format
    (head,tail) = os.path.split(input_file)
    if(tail.split('.')[-1] == 'hex'):
        # Hex format file
        try:
            app_image, startaddr = ihex_loader.load_ihex(input_file)
        except:
            return (False, "Failed to load hex file, check file format.")
        if(startaddr != (samg55.flash_address + samg55.bootloader_size + USB_BOOTLOADER_SIZE)):
            return (False, "Start address doesn't match configuration.")
        
    elif(tail.split('.')[-1] == 'bin'):
        # Binary format file
        app_image = load_bin(input_file)
        startaddr = samg55.flash_address + samg55.bootloader_size + USB_BOOTLOADER_SIZE
    else:
        # Can't read any other type
        return (False, "Unknown file type. Only accepts *.bin or *.hex files.")

    # Fit to 32-byte alignment, but leave 4 bytes at the end for CRC-32
    while (len(app_image) % 32) != 28:
        app_image.append(0xff)

    # Place application length at predetermined slot. Note: this should be empty of any program
    # data. Make sure your linker script skips this section and leaves it empty.
    if(not check_is_blank_or_ff(app_image[samg55.vector_offset:(samg55.vector_offset + 4)])):
        return (False, "Vector location is not blank. Data exists at start location + 0x400.")
    app_image[samg55.vector_offset] = (int(len(app_image)) >> 0) & 0xff
    app_image[samg55.vector_offset+1] = (int(len(app_image)) >> 8) & 0xff
    app_image[samg55.vector_offset+2] = (int(len(app_image)) >> 16) & 0xff
    app_image[samg55.vector_offset+3] = (int(len(app_image)) >> 24) & 0xff
    
    # Place calculated CRC in the last 4 bytes, after the length of the application
    # Calculate CRC on application
    crccalc = CrcCalculator(Crc32.BZIP2, True)
    crc32val = crccalc.calculate_checksum(bytes(app_image))
    # crc32 = calc_end_crc(app_image, len(app_image))
    app_image.append(int((crc32val >> 0) & 0xff))
    app_image.append(int((crc32val >> 8) & 0xff))
    app_image.append(int((crc32val >> 16) & 0xff))
    app_image.append(int((crc32val >> 24) & 0xff))

    # Save new Hex file
    ihex_loader.save_ihex(output_file, app_image, startaddr)
    return (True, "")


def file_pick(filename):
    (head,tail) = os.path.split(filename)

    # Figure out some useful info about this file
    if(tail.split('.')[-1] == 'hex'):
        # Hex format file
        try:
            app_image, startaddr = ihex_loader.load_ihex(self.filen)
        except:
            print("Error: Failed to load hex file, check file format.")
            return

        # Check app start address
        if(startaddr != (samg55.flash_address + samg55.bootloader_size + USB_BOOTLOADER_SIZE)):
            print("Error: Start address doesn't match configuration (0x{:08X})".format((samg55.flash_address + samg55.bootloader_size + USB_BOOTLOADER_SIZE)))
            return
        print('Hex file loaded: {} bytes, starting at 0x{:08X}'.format(len(app_image), startaddr))
    elif(tail.split('.')[-1] == 'bin'):
        # Binary format file
        app_image = load_bin(filename)
        startaddr = samg55.flash_address + samg55.bootloader_size + USB_BOOTLOADER_SIZE
        print('Bin file loaded: {} bytes, starting at 0x{:08X}'.format(len(app_image), startaddr))
    else:
        print('Error: App format must be either .hex or .bin')
        return

    # Check app length
    if(len(app_image) + samg55.bootloader_size + USB_BOOTLOADER_SIZE > samg55.flash_size - 4):
        # Note: the minus 4 at the end comes from needing 4 bytes for the CRC-32 after the app firmware image
        print("Error: Application length exceeds flash region.")
        return

    return app_image, startaddr

def create_usb_file(self, filename, suffix):
    app_image, startaddr = file_pick(filename)
    # Split the filename so we can add a suffix like "_FW" to the end
    (head, tail) = os.path.split(filename)
    tail = tail.split('.')[0]+suffix+'.hex'
    outfile = os.path.join(head,tail)
    
    # Fit to 32-byte alignment, but leave 4 bytes at the end for CRC-32
    while (len(app_image) % 32) != 28:
        app_image.append(0xff)

    # Place application length at predetermined slot. Note: this should be empty of any program
    # data. Make sure your linker script skips this section and leaves it empty.
    if(not check_is_blank_or_ff(app_image[samg55.vector_offset:(samg55.vector_offset + 4)])):
        print("Error: App length location in firmware is not blank. Data exists at start location + 0x400.")
        return
    app_image[samg55.vector_offset] = (int(len(app_image)) >> 0) & 0xff
    app_image[samg55.vector_offset+1] = (int(len(app_image)) >> 8) & 0xff
    app_image[samg55.vector_offset+2] = (int(len(app_image)) >> 16) & 0xff
    app_image[samg55.vector_offset+3] = (int(len(app_image)) >> 24) & 0xff

    # Place calculated CRC in the last 4 bytes, after the data of the application
    # Compute CRC
    crccalc = CrcCalculator(Crc32.BZIP2, True)
    crc32val = crccalc.calculate_checksum(bytes(app_image))
    app_image.append(int((crc32val >> 0) & 0xff))
    app_image.append(int((crc32val >> 8) & 0xff))
    app_image.append(int((crc32val >> 16) & 0xff))
    app_image.append(int((crc32val >> 24) & 0xff))

    # Save new Hex file
    ihex_loader.save_ihex(outfile, app_image, startaddr)
    print('Firmware file created! {}'.format(outfile))

    create_file_pass, ret_msg = create_fw_image_for_usb_bootloader(self.filen,outfile)
    if(create_file_pass):
        self.statusbar.config(text='Firmware file created! {}'.format(outfile))
    else:
        self.statusbar.config(text='Failed to create firmware for USB bootloader: {}'.format(ret_msg))

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Creates firmware hex for USB booloader.")
    parser.add_argument('-s', '--suffix', help='suffix to add to filename before the .hex (default=_FW)')
    parser.add_argument('file_name', help='Input filename')

    args = parser.parse_args()
    
    (head,tail) = os.path.split(args.file_name)
    
    if(args.suffix):
        tail_outfile = tail.split('.')[0]+args.suffix+'.hex'
    else:
        tail_outfile = tail.split('.')[0]+'_FW.hex'
    outfile = os.path.join(head,tail_outfile)
    
    passed, error_str = create_fw_image_for_usb_bootloader(args.file_name, outfile)
    
    if(not passed):
        print(error_str)
    else:
        print('Firmware file created! {}'.format(outfile))