'''
/** Original work Copyright 2024 damiano IU3QGD
 * https://to-be-defined
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *     See the License for the specific language governing permissions and
 *     limitations under the License.

 * Addition to the licence: NO company with more than 50employees can use this SW and you are NOT entitled to store this software 
 * on ANY storage that in some way attempts to monetize it (eg: github)
 * corporations and banks are too rich and too powerful, they MUST be split and ALL the money given back to the state


I need a global holder of the eeprom, so I can ask for blocks
Eeprom is divided in blocks of 16 bytes, it is best to keep it like this

This is the new V3 radio, puya with 128Kflash
  bootloader 0518 0020
  fw_vers: 7.00.07
  chip_id: 4434504102303832372d5900780a0156
  fw_file: QS-Kradio-7.00.07.bin
  
This is the new K1 radio uv-K1(8) V3
  bootloader 0518 0020
  fw_vers: 7.02.02
  chip_id: 4434504102303832372ea90078390156
  fw_file: QS-Kradio-7.02.02.bin
  
The old QS K5 is 
    QS-Kradio-2.00.06.bin
    
  
    
  
  


'''
from __future__ import annotations

import struct
from typing import List, Tuple

from glob_fun import bits_getBooleanBit, bits_setBooleanBit
import glob_ippqs
import qpystat
from uvk5_lib import QS_read_EE_Parser, UVK5_FLASH_BLOCK_LEN


# =========================================================================================
# NOTE that allocated blocks must NEVER be detached from the list, you can change the content but NOT detach
class GlobEeprom():
    
    ee_blocks_max=512   # from 0 to 1FF0 there are this number of blocks with 16 bytes len
    ee_block_len=16     # NO, python does NOT have constants, it is just a magic world where there are plenty of assholes

    # ------------------------------------------------------------------------------------    
    def __init__(self, stat : qpystat.Qpystat):
        self.stat = stat
        
        self._blocks_list : List[EEblock] = [] 
        
        for b_index in range(self.ee_blocks_max):
            self._blocks_list.append(EEblock(b_index))
        

    # ------------------------------------------------------------------------------------
    def _println(self, msg : str ):
        self.stat.app_println(msg)
        
    # ------------------------------------------------------------------------------------
    # return a block holding data for the given address
    # NOTE that the address can be anywhere within the block
    def get_block_from_address(self, b_start : int ) -> EEblock:
        
        w_block_idx = int(b_start / self.ee_block_len)
            
        return self._blocks_list[w_block_idx]
        
        
    # ------------------------------------------------------------------------------------
    # Update the block content taking the bytearray from the incoming block
    # NOTE that the current block is NOT replaced, what is replaced is the data
    # NEEDED to implement signaling of data changed...
    def _set_block_from_radio(self, i_block : EEblock ):
        if not i_block:
            return 

        a_block = self.get_block_from_address(i_block.ee_start_a)
        
        if not a_block:
            return
        
        a_block.set_data_from_block(i_block)
        
        
    # ----------------------------------------------------------------------
    # this will receive a response from the radio and attempt to update a local eeprom block
    # this is NOT called from a swing thread, do NOT do GUI operations here
    def eepromUpdateFromRadio (self, chunk : glob_ippqs.Qsk5_res_Eeprom ):
        
        if chunk.isReadResponse():
            self._set_block_from_radio(chunk.ablock)
            
    # ----------------------------------------------------------------------
    # for quansheng protocol
    # return True if all is well
    def eepromUpdateFromRadioQuansheng(self, chunk : QS_read_EE_Parser ) ->bool:
        if not chunk.isValid():
            return False
        
        a_block = self.get_block_from_address(chunk.ee_start_a)
        
        if not a_block:
            return False
        
        a_block.set_data_from_block(chunk.get_EEBlock())
        
        return True
        
        
    # --------------------------------------------------------------------------
    # AHHHHHHHH another CRAP PYTHON !!!! you can write ee_block.isUserChanged-True and it does NOT complain
    # NOTHING, SHIT SHIT SHIT 
    # needed to upload a downloaded config
    def markAllBlocksUpdated(self):

        for ee_block in self._blocks_list:
            ee_block.isUserChanged=True
        
    # --------------------------------------------------------------------------
    # should go trough all blocks and request block write for the changed blocks
    # NOTE that the write is QUEUED to the polling task !
    def ee_queueWriteToRadio(self, write_calibration : bool ):

        for ee_block in self._blocks_list:
            
            if not ee_block.isUserChanged:
                continue

            # to write calibration data, there should be an official request
            if ee_block.ee_start_a >= 0x1E00 and not write_calibration:
                continue

            if not ee_block.isLoadedFromRadio:
                self._println("You MUST load a working eeprom BEFORE saaving")
                return
            
            cmd = glob_ippqs.Qsk5_req_write_eeprom(ee_block.ee_start_a, ee_block.block_bytes_len, ee_block.ee_bytearray)
            self.stat.qconnect.queuePut(cmd)
            

    # --------------------------------------------------------------------------
    # should go trough all blocks and write them to file
    def ee_saveToFile(self, ffname : str ):
        
        try:
            with open(ffname, 'wb') as bin_file:

                for ee_block in self._blocks_list:
                    bin_file.write(ee_block.ee_bytearray)
                
        except Exception as exc :
            self._println("ee_saveToFile "+str(exc))



    # --------------------------------------------------------------------------
    # 
    def eepromLoadFromFile(self, filename : str ):
        
        try:
            with open(filename, 'rb') as bin_file:

                for row in self._blocks_list:
                    ee_block : EEblock = row

                    ee_bdata = bin_file.read(self.ee_block_len)
                    ee_block.ee_bytearray=bytearray(ee_bdata)
                    
                    # mark this block as user changed since it comes from who knows where
                    ee_block.isUserChanged=True
                    
                    # this is kind of loaded from radio ...
                    ee_block.isLoadedFromRadio=True
                
        except Exception as exc :
            self._println("eepromLoadFromFile "+str(exc))
        

    # --------------------------------------------------------------------------
    def get_loadedLen(self) -> int:

        read_len=0
        
        for row in self._blocks_list:
            ee_block : EEblock = row

            if ee_block.isLoadedFromRadio:
                read_len = read_len + self.ee_block_len
                
        return read_len

# ==========================================================================
# I need a generic block of memory, to send data around
# this will be used for both read and write to EEPROM
# This class should be preallocated, with no content and filled with content on eeprom read from radio
# it should also keep a flag "changed", so I can upload only what is changed
# NOTE you can HOLD an EEblock but NOT the associated byte array !!!!

class EEblock ():

    # it is simpler to fix a block len to 16 bytes, to avoid overlapping issues in writing eeprom
    # and.... this would be a CONSTANT if crap python had constants
    block_bytes_len=16

    # -----------------------------------------------------------------------
    # another CRAP from pythin, cannot refer to a class constant, since, they are not constant
    # In any case, it is not possible to have a null ee_bytearray
    def __init__(self, block_index : int,  ee_bytearray=bytearray(16) ):
        
        self.block_index = block_index
        
        # this should be a final value, but there is no such thing...
        self.ee_start_a= block_index * self.block_bytes_len
        
        # because crap python does not really know what is incoming
        self.ee_bytearray=bytearray(ee_bytearray)
        
        # at creation it is not changed by the user
        self.isUserChanged=False
        
        # initial block content is not set
        self.isLoadedFromRadio=False

    # ------------------------------------------------------------------------
    # call this to clear the content of the bytearray as if it was a fresh eeprom
    def clearBytearray(self, default_value=0xFF ):
        self.ee_bytearray=bytearray([default_value]*self.block_bytes_len)
        self.isUserChanged=True

    # ------------------------------------------------------------------------
    # Import the other block bytearray into this one bytearray
    # this is normally used while reading the radio EEPROM
    # @return True if the blocks are compatible and data has been imported
    
    def set_data_from_block(self, o_block : EEblock ) -> bool:
        if not o_block:
            return False
        
        if o_block.ee_start_a != self.ee_start_a:
            return False

        self.ee_bytearray = o_block.ee_bytearray

        # when it is loaded from somewhare
        self.isUserChanged=False
        
        self.isLoadedFromRadio=True
        
        return True

    # ---------------------------------------------------------------------
    # use this instead of directly assigning the bytearray
    def set_bytearray(self, a_bytes : bytes ):
        if not a_bytes:
            return
        
        if len(a_bytes) != len(self.ee_bytearray):
            return 
        
        if self.ee_bytearray == a_bytes:
            return 
        
        self.ee_bytearray=bytearray(a_bytes)
        
        self.isUserChanged=True
        
        
        
        
    
    # -------------------------------------------------
    # return the uint16 at given index, if not found, return zero
    # if the index is wrong it will raise exception
    def get_uint16(self, start_from : int ) -> int:

        a_tuple=struct.unpack_from('<H', self.ee_bytearray, start_from)

        if a_tuple:
            return a_tuple[0]
        
        return 0
        
    # -------------------------------------------------
    # set the given value, mark as changed if appropriate
    def set_uint16(self, start_from : int, u_value : int ):
        
        previous = self.get_uint16(start_from)
        
        if previous == u_value:
            return
        
        struct.pack_into('<H', self.ee_bytearray, start_from, u_value)
        
        self.isUserChanged = True
        
    
    # -------------------------------------------------
    # @return the byte at given index
    # note that this is ALWAYS an unsigned byte since a bytearray handles UNSIGNED bytes only
    # if the index is wrong it will raise exception
    # if you wish to deal with signed bytes use
    # ee_blk.eeprom_pack_into('<b', self._ee_byte_index, avalue)
    # touples=ee_blk.unpack('<b',self._ee_byte_index)
    def get_byte(self, byte_index : int ) -> int:
        return self.ee_bytearray[byte_index];

    # -------------------------------------------------
    # no, there is no byte type, everything is an int until you cannot push it anymore
    # plain stupid, ah, I know, pass an array of bytes of len 1.... do you also want a pine cone in your ass with it ?
    # this is a user setting the byte, so, it is possible to check if the byte being set is different than the one in memory
    # NOTE that the byte_value is ONLY unsigned, so 0 to 255
    def set_user_byte(self, byte_index : int, byte_value : int ):

        if self.ee_bytearray[byte_index] == byte_value:
            return
        
        self.ee_bytearray[byte_index] = byte_value
        
        self.isUserChanged = True


    # ------------------------------------------------
    # return as a boolean the bit in the given byte of this block
    def get_boolean(self, byte_index : int, bit_shift : int ) -> bool:
        abyte = self.get_byte(byte_index)
        
        return bits_getBooleanBit(abyte, bit_shift)


    # ------------------------------------------------
    # sets the value as boolean bit
    def set_user_boolean(self, byte_index : int, bit_shift : int, b_Val : bool ):

        abyte = self.get_byte(byte_index)
        
        n_val = bits_setBooleanBit(abyte, bit_shift, b_Val)
        
        if abyte == n_val:
            return 
        
        self.ee_bytearray[byte_index] = n_val

        self.isUserChanged = True

    # -------------------------------------------------
    # if you need to read something in a non simple way....
    # NOTE that it WILL return what struct.unpack will return, so an array of values
    # I is for interger, unsigned
    # h is for short, signed
    # H un signed short
    # x pad byte
    def eeprom_unpack(self, a_format : str, start_from = 0 ) -> Tuple:
        #w_size = struct.calcsize(a_format)
        return struct.unpack_from(a_format, self.ee_bytearray, start_from)
        
    # -------------------------------------------------
    # if you need to pack something in a non simple way....
    def eeprom_pack_into(self, a_format : str, start_from : int , *varargs ) :
        
        a_copy : bytearray = bytearray(self.ee_bytearray)
        
        struct.pack_into(a_format, a_copy, start_from, *varargs)
        
        # if the resulting value is the same, there is nothing to do
        if self.ee_bytearray == a_copy:
            return
        
        self.ee_bytearray = a_copy
        self.isUserChanged=True
        

    # -------------------------------------------------
    # It is assumed that a full bytearray is given
    # this is a user setting the new value
    
    def set_user_bytearray (self, b_array : bytearray ):
    
        if self.block_bytes_len != len(b_array):
            raise Exception("b_array len is NOT right "+str(len(b_array)))
        
        if self.ee_bytearray == b_array:
            return
            
        self.ee_bytearray = b_array
        
        self.isUserChanged = True
    
    # -------------------------------------------------
    # return a string by doing a decode ascii on the given range
    # if END is not given the whole block will be split
    def get_string_old(self, start=0, end=None) -> str:

        if end:
            a_string=self.ee_bytearray[start:end].decode('ascii',errors='ignore')
        else:
            a_string=self.ee_bytearray.decode('ascii',errors='ignore')
            
        # this actually works to remove trailing zeros from the string
        # because, you know, the codepoint 0 is an ASCII char .....
        # idiots, reinventing the wheel as a pentagon
        
        return a_string.split('\x00', 1)[0]            
        
    # -------------------------------------------------
    # return a string by doing a decode ascii on the given range
    # NOTE that the second parameter is a length and not an index (as in python)
    def get_string_new(self, start_idx : int , src_length : int ) -> str:

        a_split = self.ee_bytearray[start_idx:start_idx+src_length]
        a_string=a_split.decode('ascii',errors='ignore')
            
        # this actually works to remove trailing zeros from the string
        # because, you know, the codepoint 0 is an ASCII char .....
        # idiots, reinventing the wheel as a pentagon
        
        return a_string.split('\x00', 1)[0]            


    # ------------------------------------------------------------------------------
    # this instead sets a string within the block starting at a given idx for the given len
    # NO optional parameters since I need to know what I am doing 
    def set_user_string_new(self, astring : str,  start_idx : int , max_len : int ):
        
        s_encoded=bytearray(astring.encode('ascii',errors='ignore'))

        # copy the current byte array
        n_barray = bytearray(self.ee_bytearray)
            
        ins_idx=start_idx
        src_idx=0
        src_len=len(s_encoded)
        insm_idx=0

        # in any case, do NOT go outside the bytearray len
        while ins_idx < self.block_bytes_len:

            # if I am within the source string length
            if src_idx < src_len:
                n_barray[ins_idx] = s_encoded[src_idx]
            # I am done with the source, do I have to add some zeros ?
            elif insm_idx < max_len:
                n_barray[ins_idx]=0
                
            ins_idx += 1
            src_idx += 1
            insm_idx += 1 
            
        if n_barray != self.ee_bytearray:             
            self.ee_bytearray = n_barray
            self.isUserChanged = True


    # --------------------------------------------------
    # this is the equivalent of toString()
    def __str__(self):
        return str(self.ee_start_a)


# =========================================================================
# It is here since this is in charge of communication with radio
class Uvk5Firmware():
    
    # -------------------------------------------------------------
    def __init__(self, stat : qpystat.Qpystat):
        self._stat = stat
        self._blocklist : list[UVK5_fw_block] = []
        
    # -------------------------------------------------------------
    def _println(self, msg : str ):
        self._stat.app_println(msg)
                
    # -------------------------------------------------------------
    # You could load different FW
    def blocklist_clear(self):
        self._blocklist.clear()
        
    # -------------------------------------------------------------
    # attempt to load the given file 
    # @return true if all is fine
    def load_from_filename(self, filename ) -> bool:
        self.blocklist_clear()
        
        try:
            with open(filename, 'rb') as bin_file:
                eof=False 
                w_address=0
                
                while not eof:
                    ee_bdata = bin_file.read(UVK5_FLASH_BLOCK_LEN)
                
                    if ee_bdata:
                        self._blocklist_append(UVK5_fw_block(w_address,ee_bdata))
                
                    w_address = w_address + UVK5_FLASH_BLOCK_LEN
                    
                    if len(ee_bdata) < UVK5_FLASH_BLOCK_LEN:
                        eof=True 

            return True
                
        except Exception as exc :
            self._println("load_from_filename "+str(exc))
            return False

    # -------------------------------------------------------------
    # load all blocks using this one
    def _blocklist_append(self, a_block : UVK5_fw_block ):
        self._blocklist.append(a_block)

    # -------------------------------------------------------------
    # For verification purposes
    def getFirmwareSize(self) -> int:
        
        a_len=0;
        
        for row in self._blocklist:
            ablock : UVK5_fw_block=row
            a_len = a_len + len(ablock.b_data) 
            
        return a_len

# ----------------------------------------------------------------
class UVK5_fw_block():
    
    def __init__(self, w_address, b_data : bytes ):
        self.w_address=w_address
        self.b_data=b_data
        
        
        



