# 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 tkinter as tk
from tkinter import ttk, messagebox, filedialog

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, "")

class fw_packager_gui:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title('Bigscreen Firmware Packager')
        self.file_picked = False
        self.filesuffixvar = tk.StringVar(self.root, "_FW")
        self.load_ui()

    def load_ui(self):
        self.content = ttk.Frame(self.root, padding=(5,5,5,5), width=400, height=300)
        self.content.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.E, tk.W))

        self.lbl_file = ttk.Label(self.content, text='Data File:')
        self.filenlbl = ttk.Label(self.content, text='Pick an application firmware file -->', relief=tk.SUNKEN, wraplength=500)
        self.lbl_suffix = ttk.Label(self.content, text='Select new file suffix:')
        self.newsuffix = ttk.Entry(self.content, exportselection=False, textvariable=self.filesuffixvar)
        self.btnFilePick = ttk.Button(self.content, text='...', command=self.file_pick)
        self.btnCreateUSBFile = ttk.Button(self.content, text='Create USB Bootloader File', command=self.create_usb_file)
        self.statusbar = ttk.Label(self.content, text='Ready', border=1, relief=tk.SUNKEN)
        self.lbl_applen_hex = ttk.Label(self.content, text='App Length (hex):')
        self.lbl_applen_dec = ttk.Label(self.content, text='App Length (decimal):')
        self.lbl_appstart_hex = ttk.Label(self.content, text='App Start:')
        self.lbl_crc = ttk.Label(self.content, text="CRC-32:")
        self.disp_applen_hex = ttk.Label(self.content, text='-', border=1, relief=tk.SUNKEN)
        self.disp_applen_dec = ttk.Label(self.content, text='-', border=1, relief=tk.SUNKEN)
        self.disp_appstart_hex = ttk.Label(self.content, text='-', border=1, relief=tk.SUNKEN)
        self.disp_crc = ttk.Label(self.content, text='-', border=1, relief=tk.SUNKEN)
        
        self.default_lbl_bg = self.disp_applen_hex.cget('background')
        self.default_lbl_fg = self.disp_applen_hex.cget('foreground')
        self.error_lbl_bg = "#{:02X}{:02X}{:02X}".format(249,137,70) # kind of a reddish hue
        self.error_lbl_fg = "#{:02X}{:02X}{:02X}".format(0,0,0) # pure ol' black

        self.lbl_file.grid(column=0, row=0, sticky="nsew", padx=5, pady=5)
        self.filenlbl.grid(column=1, row=0, sticky="nsew", padx=5, pady=5)
        self.btnFilePick.grid(column=2, row=0, sticky="nsew",padx=5, pady=5)
        self.lbl_applen_dec.grid(column=0, row = 1, sticky="nsew", padx=5, pady=5)
        self.lbl_applen_hex.grid(column=0, row = 2, sticky="nsew", padx=5, pady=5)
        self.lbl_appstart_hex.grid(column=0, row = 3, sticky="nsew", padx=5, pady=5)
        self.lbl_crc.grid(column=0, row = 4, sticky="nsew", padx=5, pady=5)
        self.disp_applen_dec.grid(column=1, row = 1, sticky="nsew", padx=5, pady=5)
        self.disp_applen_hex.grid(column=1, row = 2, sticky="nsew", padx=5, pady=5)
        self.disp_appstart_hex.grid(column=1, row = 3, sticky="nsew", padx=5, pady=5)
        self.disp_crc.grid(column=1, row = 4, sticky="nsew", padx=5, pady=5)
        self.lbl_suffix.grid(column=0, row = 5, sticky="nsew", padx=5, pady=5)
        self.newsuffix.grid(column=1, row = 5, sticky="nsew", padx=5, pady=5)
        self.btnCreateUSBFile.grid(column=2, row=5, sticky="nsew", padx=5, pady=5)
        self.statusbar.grid(column=0, row=6, columnspan=3, sticky="nsew", padx=5, pady=5)

        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)

        self.content.columnconfigure(0, weight=0)
        self.content.columnconfigure(1, weight=1)
        self.content.columnconfigure(2, weight=0)
        self.content.rowconfigure(0, weight=1)
        self.content.rowconfigure(1, weight=0)
        self.content.rowconfigure(2, weight=0)
        self.content.rowconfigure(3, weight=0)
        self.content.rowconfigure(4, weight=0)
        self.content.rowconfigure(5, weight=0)
        self.content.rowconfigure(6, weight=0)

    def start_main_loop(self):
        self.root.mainloop()

    def file_pick(self):
        self.filen = filedialog.askopenfilename(filetypes=[('HEX/BIN',"*.hex *.bin")],initialdir=os.getcwd())
        if(len(self.filen) > 0):
            (head,tail) = os.path.split(self.filen)
            self.filenlbl.config(text=tail)
            self.statusbar.config(text='Ready')
            # Figure out some useful info about this file
            if(tail.split('.')[-1] == 'hex'):
                # Hex format file
                try:
                    self.app_image, self.startaddr = ihex_loader.load_ihex(self.filen)
                except:
                    self.statusbar.config(text="Error: Failed to load hex file, check file format.")
                    self.file_picked = False
                    return
                self.file_picked = True
                self.disp_appstart_hex.config(text='0x{:08X}'.format(self.startaddr))

                # Check app start address
                if(self.startaddr != (samg55.flash_address + samg55.bootloader_size + USB_BOOTLOADER_SIZE)):
                    self.statusbar.config(text="Error: Start address doesn't match configuration (0x{:08X})".format((samg55.flash_address + samg55.bootloader_size + USB_BOOTLOADER_SIZE)))
                    self.file_picked = False
                    self.disp_appstart_hex.config(background=self.error_lbl_bg, foreground=self.error_lbl_fg)
                else:
                    self.disp_appstart_hex.config(background=self.default_lbl_bg, foreground=self.default_lbl_fg)
            elif(tail.split('.')[-1] == 'bin'):
                # Binary format file
                self.app_image = load_bin(self.filen)
                self.file_picked = True
                self.startaddr = samg55.flash_address + samg55.bootloader_size + USB_BOOTLOADER_SIZE
                self.disp_appstart_hex.config(text='0x{:08X} (assumed)'.format(self.startaddr))
                self.disp_appstart_hex.config(background=self.default_lbl_bg, foreground=self.default_lbl_fg)
            else:
                self.statusbar.config(text='Error: App format must be either .hex or .bin')
                self.file_picked = False
                return

            self.disp_applen_hex.config(text='0x{:X}'.format(len(self.app_image)))
            self.disp_applen_dec.config(text=str(len(self.app_image)))

            # Check app length
            if(len(self.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
                self.statusbar.config(text="Error: Application length exceeds flash region.")
                self.file_picked = False
                self.disp_applen_hex.config(background=self.error_lbl_bg, foreground=self.error_lbl_fg)
                self.disp_applen_dec.config(background=self.error_lbl_bg, foreground=self.error_lbl_fg)
            else:
                self.disp_applen_hex.config(background=self.default_lbl_bg, foreground=self.default_lbl_fg)
                self.disp_applen_dec.config(background=self.default_lbl_bg, foreground=self.default_lbl_fg)
            

            

    def create_usb_file(self):
        if(self.file_picked):
            # Split the filename so we can add a suffix like "_FW" to the end
            (head, tail) = os.path.split(self.filen)
            tail = tail.split('.')[0]+self.filesuffixvar.get()+'.hex'
            outfile = os.path.join(head,tail)

            # Fit to 32-byte alignment, but leave 4 bytes at the end for CRC-32
            while (len(self.app_image) % 32) != 28:
                self.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(self.app_image[samg55.vector_offset:(samg55.vector_offset + 4)])):
                self.statusbar.config(text="Error: App length location in firmware is not blank. Data exists at start location + 0x400.")
                return
            self.app_image[samg55.vector_offset] = (int(len(self.app_image)) >> 0) & 0xff
            self.app_image[samg55.vector_offset+1] = (int(len(self.app_image)) >> 8) & 0xff
            self.app_image[samg55.vector_offset+2] = (int(len(self.app_image)) >> 16) & 0xff
            self.app_image[samg55.vector_offset+3] = (int(len(self.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)
            self.crc32val = crccalc.calculate_checksum(bytes(self.app_image))
            self.disp_crc.config(text='0x{:08X}'.format(self.crc32val))
            self.app_image.append(int((self.crc32val >> 0) & 0xff))
            self.app_image.append(int((self.crc32val >> 8) & 0xff))
            self.app_image.append(int((self.crc32val >> 16) & 0xff))
            self.app_image.append(int((self.crc32val >> 24) & 0xff))

            # Save new Hex file
            ihex_loader.save_ihex(outfile, self.app_image, self.startaddr)
            self.statusbar.config(text='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))
        else:
            self.statusbar.config(text="Please select a valid hex/bin file first.")
if __name__ == "__main__":
    widget = fw_packager_gui()
    sys.exit(widget.start_main_loop())