'''
/** 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

https://people.csail.mit.edu/hubert/pyaudio/docs/
https://docs.python.org/3.13/library/tkinter.html
https://www.tcl-lang.org/man/tcl8.6/TkCmd/contents.htm

'''


from __future__ import annotations

import asyncio
from bleak import BleakScanner, BleakClient
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.scanner import AdvertisementData
from bleak.exc import BleakDeviceNotFoundError
import datetime
import io
import ipaddress
import pathlib
import queue
import struct
from threading import Thread
import time
from tkinter import StringVar, ttk, filedialog
import traceback
import typing

from app_config import AppConfig, ConfigSavable
from glob_class import HasPrintln, IppChunkIterator, IppChunk, WisocketTcp
from glob_gui import GUI_hide_show_window, JtkWinToplevel, JtkPanelPackTop, \
    JtkPanelPackLeft, JtkButtonText, LogPanel, JtkPanelGrid, \
    JtkLabel, TV_Entry, JtkCheckbox, JtkTableColdef, JtkTable, \
    table_row_selected_background_color, JtkPanelTabbed
from glob_ippwi import Wira_req_datetime, Wira_request, Wira_res_datetime, \
    IPPW_res_datetime, IPPW_res_gpio, Wira_res_gpio, Wira_req_gpio, \
    Wira_req_play_fname, IPPW_res_wfs, Wira_res_wfs, Wira_req_record_fname, \
    Wira_req_directory, IPPW_res_dirfile, Wira_res_filename, Wira_pull_file, \
    IPPW_res_generic, Wira_res_generic, \
    Wira_push_file, IPPW_res_filechunk, Wira_res_file_pull, IPPW_WFS_FCHU_MAXLEN, \
    Wira_req_file_chunk, Wira_wfs_delete_file
import qpystat


UART_SERVICE_UUID = "0000abf0-0000-1000-8000-00805f9b34fb"

UART_RX_CHAR_UUID = "0000abf1-0000-1000-8000-00805f9b34fb"

wfsdir_idx="wfsdir_idx"
wfsdir_fname="wfsdir_fname"
wfsdir_fsize="wfsdir_fsize"
wfsdir_fdate="wfsdir_fdate"
    
# -------------------------------------------------------
# Generic way to define a map from a column name to some properties        
qschcols_map = { 
    wfsdir_idx     : JtkTableColdef(wfsdir_idx    ,'Nr.',True, 40), 
    wfsdir_fname   : JtkTableColdef(wfsdir_fname  ,"Name",False,200, True),
    wfsdir_fsize   : JtkTableColdef(wfsdir_fsize  ,"Size Bytes",True, 100),
    wfsdir_fdate   : JtkTableColdef(wfsdir_fdate  ,"C Date",False, 90),
    }

# ====================================================================
# I need a class to handle data in teh directory
class WiradioFilename():
    
    def __init__(self, res_dirfile : Wira_res_filename ):
        self.row_idx  = res_dirfile.cur_dir_index
        self.filename = res_dirfile.fname
        self.filesize = res_dirfile.dirf_len_bytes
        self.filedata = res_dirfile.creation_date
        
    # -----------------------------------------------------------------
    # return the list of values to show into the table, the GUI table
    # NOTE that the order has to be consistent with the GUI definition
    def getValuesForTreeview(self):
        
        risul = (
            self.row_idx,
            self.filename,
            self.filesize,
            self.filedata
            )
            
        return risul         
        

# ====================================================================
# this is GUI and a Thread, because ... whatever
class Wiradio_gui (Thread,ConfigSavable,GUI_hide_show_window, HasPrintln):

    _cfg_item_name = 'wiradio_cfg'

    # ----------------------------------------------------------------
    def __init__(self, stat : qpystat.Qpystat):
        Thread.__init__(self,name="Wiradio TCP tx rx",daemon=True)
        
        self.stat = stat
        
        self.stat.appconfig.addMyselfToSavables(self)

        self._wira_oqueue : queue.Queue = queue.Queue()
        self._tcp_socket : WisocketTcp = WisocketTcp()

        self._file_list : typing.List[WiradioFilename] = []

        self._toplevel = JtkWinToplevel("Wiradio Window")
        
        a_panel=JtkPanelPackTop(self._toplevel, padding=2 )     

        # you can switch between the Contol and Config
        a_panel.addItem(self._new_wiradio_tabbed_panel(a_panel))
        
        # log is always present
        self._wiradio_log = LogPanel(a_panel,"WiRadio Log")
        a_panel.addItem(self._wiradio_log, fill='both',expand=True)
        
        a_panel.pack(fill='both',expand=True)

        self.GUI_hide_window() 
        
        self._println("Wiradio init complete")

    # -------------------------------------------------------------------
    def _queue_request (self, a_request : Wira_request):
        self._wira_oqueue.put(a_request)

    # -------------------------------------------------------------------
    def _new_wiradio_tabbed_panel(self, parent) -> ttk.Widget:

        a_panel = JtkPanelTabbed(parent)
        a_panel.addItem(self._newWiradioControlPanel(a_panel),' Control ')
        a_panel.addItem(self._newWiradioBtlePanel(a_panel),' Bluetooth ')
        
        return a_panel
        
    # -------------------------------------------------------------------
    # holds both the commands and the table
    def _newWiradioControlPanel(self,parent) -> ttk.Frame:

        a_panel = JtkPanelPackTop(parent)
        a_panel.addItem(self._newWiradioCommandsPanel(a_panel))
        a_panel.addItem(self._newSdfilesTablePanel(a_panel))

        return a_panel

    # -------------------------------------------------------------------
    def _newSdfilesTablePanel(self, parent) -> ttk.Frame:
        
        self._dirlist_tview = JtkTable(parent, qschcols_map );
        a_panel = self._dirlist_tview.getComponent()
        a_panel.pack(fill='both', expand=True)

        tk_tbl : ttk.Treeview = self._dirlist_tview.getTable()
        
        tk_tbl.tag_configure('Updated',  background=table_row_selected_background_color)
        tk_tbl.bind('<<TreeviewSelect>>', self._jtable_row_selected)
#        tk_tbl.bind('<Control-c>', self._jtable_ctrl_c)
#        tk_tbl.bind('<Control-x>', self._jtable_ctrl_x)
#        tk_tbl.bind('<Control-v>', self._jtable_ctrl_v)
#        tk_tbl.bind('<Delete>', self._jtable_delete)
        
        return a_panel

    # -------------------------------------------------------------------
    # Called when a row is selected in the tree
    # note that you NEED selectmode='browse' to have a single selection
    # IF multiple rows can be selected with option selectmode='extended' you get a list of selected items that can be not in sequence
    # you can use SHIFT or CTRRL + click to add or remove selected elements
    def _jtable_row_selected(self, sel_coord):
        
        a_sel = self._dirlist_tview.getSelected_iid()
        
        if not a_sel:
            return 
        
        #self._println(str(a_sel))
        
        row_idx : int = int(a_sel[0])
        
        sel_item : WiradioFilename = self._file_list[row_idx]

        self._selected_fname.set(sel_item.filename)


    # -------------------------------------------------------------------
    # prints something on this window log
    def _println(self, msg):
        self._wiradio_log.println(msg)    
        
    # --------------------------------------------------------------------
    # show a window that has been hidden using the withdraw method
    def GUI_show_window(self):
        self._toplevel.deiconify()    

    # --------------------------------------------------------------------
    # hide completely a window
    def GUI_hide_window(self):
        self._toplevel.withdraw()    

    # -----------------------------------------------------------
    # Mostly a reminder that this method is available everywhere    
    def _runOnGuiIdle(self, func, *args ):
        self._toplevel.after_idle(func, *args)

    # -------------------------------------------------------------------
    # implement the config savable
    def appImportConfig(self, cfg : AppConfig ):
        adict = cfg.getDictValue(self._cfg_item_name, {})
        
        try:
            self._println("geometry "+self._toplevel.geometry())

            self._toplevel.setGeometry(adict['gui_cfg'])
#            self._qsaudio_linein_dev.set(adict['qsaudio_linein_dev'])    

        except Exception as _exc :
            pass            

    # -------------------------------------------------------------------
    # implement the config savable
    def appSaveConfig(self, cfg : AppConfig ):
        # get the current dictionary, if any
        adict = cfg.getDictValue(self._cfg_item_name, {})

        adict['gui_cfg'] = self._toplevel.getGeometry()
#        adict['qsaudio_linein_dev'] = self._qsaudio_linein_dev.get()

        cfg.setDictValue(self._cfg_item_name, adict )            

    # -------------------------------------------------------------------
    def _newGpioPanel(self, parent_panel, on_change_fun ):

        a_panel=JtkPanelPackLeft(parent_panel)
        
        self._gpio_ptt=JtkCheckbox(a_panel, 'PTT', on_change_fun)
        a_panel.addItem(self._gpio_ptt)
        
        self._gpio_A=JtkCheckbox(a_panel, 'GPIO A', on_change_fun)
        a_panel.addItem(self._gpio_A)

        self._gpio_B=JtkCheckbox(a_panel, 'GPIO B', on_change_fun)
        a_panel.addItem(self._gpio_B)

        self._gpio_C=JtkCheckbox(a_panel, 'GPIO C', on_change_fun)
        a_panel.addItem(self._gpio_C)

        self._gpio_D=JtkCheckbox(a_panel, 'GPIO D', on_change_fun)
        a_panel.addItem(self._gpio_D)
        
        return a_panel

    # -------------------------------------------------------------------
    def _newGpinPanel(self, parent_panel):

        a_panel=JtkPanelPackLeft(parent_panel)
        
        self._gpin_ptt=JtkCheckbox(a_panel, 'PTT in')
        a_panel.addItem(self._gpin_ptt)
        
        self._gpin_b=JtkCheckbox(a_panel, 'GPIO B')
        a_panel.addItem(self._gpin_b)

        self._gpin_c=JtkCheckbox(a_panel, 'GPIO C')
        a_panel.addItem(self._gpin_c)

        self._gpin_d=JtkCheckbox(a_panel, 'GPIO D')
        a_panel.addItem(self._gpin_d)

        self._gpin_sw2=JtkCheckbox(a_panel, 'GPIO SW2')
        a_panel.addItem(self._gpin_sw2)
        
        return a_panel

    # -------------------------------------------------------------------
    # this handles Bluetooth LE and the associated commands
    def _newWiradioBtlePanel(self, parent) -> ttk.Frame:

        a_panel=JtkPanelGrid(parent) # ,padding='3')

        a_panel.addItem(JtkLabel(a_panel,"BT status:"))
        self._bt_status = JtkLabel(a_panel,'BT OFF',borderwidth=2,relief='ridge')
        a_panel.addItem(self._bt_status)
        a_panel.addItem(JtkButtonText(a_panel,'Start Client',command=self._click_enable_bluetooth))

        a_panel.nextRow()
        a_panel.addItem(JtkLabel(a_panel,"Wifi BSSID:"))
        self._wifi_bssid = StringVar(a_panel,'write your wifi BSSID')
        a_panel.addItem(TV_Entry(a_panel, self._wifi_bssid))
        a_panel.addItem(JtkButtonText(a_panel,'Set BSSID',command=self._click_set_wifi_bssid))

        a_panel.nextRow()
        a_panel.addItem(JtkLabel(a_panel,"Wifi Password:"))
        self._wifi_password = StringVar(a_panel,'write your wifi Password')
        a_panel.addItem(TV_Entry(a_panel, self._wifi_password))
        a_panel.addItem(JtkButtonText(a_panel,'Set Password',command=self._click_set_wifi_password))

        a_panel.nextRow()
        a_panel.addItem(JtkLabel(a_panel,"Special Command:"))
        self._wispecial_command = StringVar(a_panel,'restart')
        a_panel.addItem(TV_Entry(a_panel, self._wispecial_command))
        a_panel.addItem(JtkButtonText(a_panel,'Execute',command=self._click_wispecial_command))
        
        return a_panel

    # --------------------------------------------------------------------
    def _click_wispecial_command(self):
        a_command = self._wispecial_command.get()
        self._println("_click_wispecial_command: "+str(a_command))
        self._wibtle_thread.queue_command(a_command)

    # --------------------------------------------------------------------
    def _click_enable_bluetooth(self):
        self._println("_click_enable_bluetooth: CALL")
        self._wibtle_thread.start()
    
    # --------------------------------------------------------------------
    def _click_set_wifi_bssid(self):
        a_command = "set bssid "+self._wifi_bssid.get()
        self._println("_click_set_wifi_bssid: "+str(a_command))
        self._wibtle_thread.queue_command(a_command)

    # --------------------------------------------------------------------
    def _click_set_wifi_password(self):
        a_command = "set bsspw "+self._wifi_password.get()
        self._println("_click_set_wifi_password: "+str(a_command))
        self._wibtle_thread.queue_command(a_command)

    # -------------------------------------------------------------------
    def set_BT_status_label(self, msg : str ):
        self._bt_status.setText(msg)
    
    # -------------------------------------------------------------------
    def _newWiradioCommandsPanel(self, parent):

        a_panel=JtkPanelGrid(parent) # ,padding='3')
        
        # this variable will store whatever the user has typed
        self._wiradio_date_time = StringVar(a_panel)
        
        a_panel.addItem(JtkLabel(a_panel,"Date Time:"))
        a_panel.addItem(TV_Entry(a_panel, self._wiradio_date_time, width=20))
        
        a_panel.addItem(JtkButtonText(a_panel,'SET',command=self._click_wiradio_set_date_time))
        a_panel.addItem(JtkButtonText(a_panel,'GET',command=self._click_wiradio_get_date_time))

        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,"Digital Output:"))
        a_panel.addItem(self._newGpioPanel(a_panel, self._click_checkbox_on_change ))
        a_panel.addItem(JtkButtonText(a_panel,'SET',command=self._click_wiradio_set_gpio))

        a_panel.nextRow()
        
        a_panel.addItem(JtkLabel(a_panel,"Digital Input:"))
        a_panel.addItem(self._newGpinPanel(a_panel))
        a_panel.addItem(JtkButtonText(a_panel,'GET',command=self._click_wiradio_get_gpio))

        a_panel.nextRow()
        a_panel.addItem(JtkLabel(a_panel,"Selected File:"))
        self._selected_fname = StringVar(a_panel,'music.mp3')
        a_panel.addItem(TV_Entry(a_panel, self._selected_fname, width=20))
        a_panel.addItem(JtkButtonText(a_panel,'Play Start/Stop',command=self._click_wiradio_play_fname))
        a_panel.addItem(JtkButtonText(a_panel,'Pull',command=self._click_wiradio_pull_file))
        a_panel.addItem(JtkButtonText(a_panel,'Delete',command=self._click_wiradio_delete_file))
        
        a_panel.nextRow()
        a_panel.addItem(JtkLabel(a_panel,"Record to File:"))
        self._rec_fname = StringVar(a_panel,'rec.aac')
        a_panel.addItem(TV_Entry(a_panel, self._rec_fname, width=20))
        a_panel.addItem(JtkButtonText(a_panel,'Start/Stop',command=self._click_wiradio_record_fname))

        a_panel.nextRow()
        a_panel.addItem(JtkLabel(a_panel,"SD Card:"))
        a_panel.addItem(JtkButtonText(a_panel,'Directory',command=self._click_wiradio_get_directory))
        a_panel.addItem(JtkButtonText(a_panel,'Upload File',command=self._click_wiradio_push_file))


        return a_panel



    # --------------------------------------------------------------------
    def _click_wiradio_push_file(self):
        a_req = Wira_request("Wiradio Push File")

        file_path_str = filedialog.askopenfilename(parent=self._toplevel)
        if not file_path_str:
            self._println('NO sile selected')
            return
        
        file_path = pathlib.Path(file_path_str)
        
        # here I can bind a specific parser to this request
        a_req._special_parser = Wira_push_file_parser(self, file_path)

        a_chunk = Wira_push_file(file_path) 
        
        a_req.add_chunk(a_chunk)
        
        self._queue_request(a_req)


    # --------------------------------------------------------------------
    def _click_wiradio_delete_file(self):
        fname = self._selected_fname.get()
        
        if not fname:
            self._println('_click_wiradio_delete_file MISSING fname')
            return

        a_req = Wira_request("Wiradio Delete File")

        a_chunk = Wira_wfs_delete_file(fname)
        
        a_req.add_chunk(a_chunk)
        
        self._queue_request(a_req)


    # --------------------------------------------------------------------
    def _click_wiradio_pull_file(self):
     
        fname = self._selected_fname.get()
        
        if not fname:
            self._println('_click_wiradio_pull_file MISSING fname')
            return

        a_req = Wira_request("Wiradio Pull File")
        
        # here I can bind a specific parser to this request
        a_req._special_parser = Wira_pull_file_parser(self, fname)

        a_chunk = Wira_pull_file(fname)
        
        a_req.add_chunk(a_chunk)
        
        self._queue_request(a_req)




    # --------------------------------------------------------------------
    def _click_wiradio_get_directory(self):
        a_chunk = Wira_req_directory()
     
        a_req = Wira_request("Wiradio Directory")
        
        a_req.add_chunk(a_chunk)
        
        self._queue_request(a_req)
        
        # the list will be filled by the result
        self._file_list.clear()
        self._dirlist_tview.clear()

    # --------------------------------------------------------------------
    def _click_wiradio_record_fname(self):
        a_chunk = Wira_req_record_fname(self._rec_fname.get())
     
        a_req = Wira_request("Record Fname")
        
        a_req.add_chunk(a_chunk)
        
        self._queue_request(a_req)


    # --------------------------------------------------------------------
    def _click_wiradio_play_fname(self):
        a_chunk = Wira_req_play_fname(self._selected_fname.get())
     
        a_req = Wira_request("Play Fname")
        
        a_req.add_chunk(a_chunk)
        
        self._queue_request(a_req)

    # --------------------------------------------------------------------
    def _click_checkbox_on_change(self, new_val : bool):
        self._click_wiradio_set_gpio()
        
    # --------------------------------------------------------------------
    def _click_wiradio_set_gpio(self ):

        w_ptt = self._gpio_ptt.IsCheckedInt() | 0b10000000
        w_a   = self._gpio_A.IsCheckedInt() | 0b10000000
        w_b   = self._gpio_B.IsCheckedInt() | 0b10000000
        w_c   = self._gpio_C.IsCheckedInt() | 0b10000000
        w_d   = self._gpio_D.IsCheckedInt() | 0b10000000
        
        a_chunk = Wira_req_gpio(1,w_ptt,w_a,w_b,w_c,w_d)
     
        a_req = Wira_request("Set GPIO")
        
        a_req.add_chunk(a_chunk)
        
        self._queue_request(a_req)

    # --------------------------------------------------------------------
    def _click_wiradio_get_gpio(self):
        a_chunk = Wira_req_gpio()
     
        a_req = Wira_request("Get GPIO")
        
        a_req.add_chunk(a_chunk)
        
        self._queue_request(a_req)
        
    
    # --------------------------------------------------------------------
    # this will send the request, the reply will arrive trouch the task
    def _click_wiradio_set_date_time(self):
        self._println('_click_wiradio_set_date_time: CALL')

        a_req = Wira_request("Set date")
        
        t_src = self._wiradio_date_time.get()
        
        try:
            
            d_t : datetime.datetime = datetime.datetime.strptime(t_src,"%Y-%m-%d %H:%M:%S")
            
            self._println('set date to '+str(d_t))
        
            a_req.add_chunk( Wira_req_datetime(1,d_t.year,d_t.month,d_t.day,d_t.hour,d_t.minute,d_t.second))
        except Exception as _err:
            self._println('invalid date time source '+t_src)
            self._println('   error'+str(_err))

            a_req.add_chunk( Wira_req_datetime())
        
        self._queue_request(a_req)

    # --------------------------------------------------------------------
    # this will send the request, the reply will arrive trouch the task
    def _click_wiradio_get_date_time(self ):
        self._println('_click_wiradio_get_date_time: CALL')
        a_chunk = Wira_req_datetime()
     
        a_req = Wira_request("Request date")
        
        a_req.add_chunk(a_chunk)
        
        self._queue_request(a_req)


    # -------------------------------------------------------------------------------
    # make a new frame and put an input line in it
    def _newConfigPanel(self, parent):

        a_panel=JtkPanelGrid(parent) # ,padding='3')
        
        # this variable will store whatever the user has typed
        self._qsaudio_linein_dev = StringVar(a_panel)
        
        a_panel.addItem(JtkLabel(a_panel," Wiradio Time:"))
        a_panel.addItem(TV_Entry(a_panel, self._qsaudio_linein_dev, width=15))

        a_panel.addItem(JtkButtonText(a_panel,'SET'))
        a_panel.addItem(JtkButtonText(a_panel,'GET'))
        
         
        return a_panel
    
    # --------------------------------------------------------------
    def _wait_for_wiradio_ipaddress(self) -> ipaddress.IPv4Address:

        while True:
            n_address : ipaddress.IPv4Address = self.stat.qconnect.get_wiradio_ipv4()

            if n_address:
                self._println("Have Wiradio IP address "+str(n_address))
                return n_address
            
            time.sleep(1)


    # --------------------------------------------------------------
    # Called by the Thread, do NOT call it, use start()
    # I wish this to stop on first error, so I can investigate
    def run(self):

        # this will allocate the thread to be ready to be started
        self._wibtle_thread = Wibtle_thread(self)
        
        try_count=0
        
        while True:
            self._println ("----------> "+str(try_count))
            self._wira_open_and_poll()
            self._println ("poll errors: wait 5s")
            time.sleep(5)
            try_count+=1

        self._println ("END "+self.name)

    



    

            
            
            

    # ------------------------------------------------------
    # Attempt to open the device and do some polling
    def _wira_open_and_poll(self):

        wiradio_ip : ipaddress.IPv4Address = self._wait_for_wiradio_ipaddress()
        
        wiradio_address = str(wiradio_ip)
        
        try:
            self._tcp_socket.socket_close()
            
            addr_port = (wiradio_address,1235)
            
            # have NO timeout here, and see how it goes
            self._tcp_socket.socket_connect(addr_port)
            
            self._println("  Connected to "+wiradio_address)

            while self._wira_loop_poll_one():
                time.sleep(0.05)
        
        except Exception as _err:
            pass
            #self._println(traceback.format_exc())
            
    # ------------------------------------------------------
    # use this method to queue a command to be executed
    def _wira_loop_poll_one(self) -> bool:
        
        try:
            # try to poll the queue of commands
            self._wira_poll_queue()
            
            # then do a polling for standard values
            self._wira_poll_periodic()
    
            # TODO remove it
            time.sleep(1)
            
        except Exception as _err:
            self._println(traceback.format_exc())
            return False
            
        return True
            
    # ---------------------------------------------------------------------
    # there should be an answer to each request            
    def _receive_answer(self):
        pass
    
    
    # ------------------------------------------------------
    # Attempt to poll the queue of commands to send to quansheng
    # NOTE that now the command is also parsing the content !
    # stay here pulling commands until there are no more
    def _wira_poll_queue(self):
        
        while not self._wira_oqueue.empty():
            
            # in theory there should be something, the wait time is just to be sure to never get stuck
            request : Wira_request = self._wira_oqueue.get(True, 0.3)
        
            self._println("_wira_poll_queue: have "+str(request))
            
            request.send_request(self._tcp_socket)
            
            if request._special_parser is None:

                a_parser = Wipp_parse_packet(self)

                a_parser.read_packet(self._tcp_socket)
                
                for a_chunk in a_parser._iterator:
                    self._parse_chunk(a_chunk)
            else:
                
                self._println("Using special parser")
                
                a_parser = request._special_parser
                
                a_parser.read_packet(self._tcp_socket) 
                
                
        
    # ------------------------------------------------------
    # 
    def _parse_chunk(self, a_chunk : IppChunk):
        if a_chunk is None: 
            return
      
        chunk_id = a_chunk.chunk_id
        
        if chunk_id == IPPW_res_datetime:
            self._parse_res_datetime(Wira_res_datetime(a_chunk))
        elif chunk_id == IPPW_res_gpio:
            self._parse_res_gpio(Wira_res_gpio(a_chunk))
        elif chunk_id == IPPW_res_wfs:
            self._parse_res_wfs(Wira_res_wfs(a_chunk), None)
        elif chunk_id == IPPW_res_dirfile:
            self._parse_res_dirfile(Wira_res_filename(a_chunk))
        elif chunk_id == IPPW_res_generic:
            g_chunk = Wira_res_generic(a_chunk)
            self._println("_parse_chunk: "+str(g_chunk))
        else:
            self._println("_parse_chunk: FAIL unsupported id "+str(chunk_id))
        

    # ------------------------------------------------------
    def _parse_res_dirfile(self, a_chunk : Wira_res_filename ):
        self._println("Have res dirfile "+str(a_chunk))
        
        a_file = WiradioFilename(a_chunk)
        
        self._file_list.append(a_file)

        self._jtable_ensureVisible(a_file)
        

    # -------------------------------------------------------------------
    # At any time it should be possible to filter the entries and show them
    # Note that this may be called at the beginning , when there is nothing in the list...
    def GUI_refresh(self ):    
        
        for row in self._file_list:
            e_row : WiradioFilename = row
            
            self._jtable_ensureVisible(e_row)

    # ------------------------------------------------------------------------
    # Utility, to make code cleaner
    # NOTE that part of the magic works because the channel number is a unique key
    # This MUST be run from swing thread
    def _jtable_ensureVisible(self, a_row : WiradioFilename ):

        if self._dirlist_tview.row_exist(a_row.row_idx):
            self._jtable_refresh_using_row(a_row)
        else:
            v_values=a_row.getValuesForTreeview()
            self._dirlist_tview.row_append(a_row.row_idx,v_values)

    # ------------------------------------------------------------------------------
    # this should update the swing table content for the given row
    def _jtable_refresh_using_row(self, a_row : WiradioFilename ):

        if not a_row:
            return
            
        self._dirlist_tview.set_row(a_row.row_idx,a_row.getValuesForTreeview(),'')   

        
    # ------------------------------------------------------
    # @return true if there is eof
    def _parse_res_wfs(self, a_chunk : Wira_res_wfs, to_file ) -> bool:
        #self._println("_parse_res_wfs "+str(a_chunk))

        self._println('_parse_res_wfs unsupported wfs_cid '+str(a_chunk.wfs_cid))
        # on weird errors assume EOF ... yes, it will mess up the decoding, possibly
        return True

    # ------------------------------------------------------
    def _parse_res_gpio(self, a_chunk : Wira_res_gpio ):
        self._println("Have GPIO "+str(a_chunk))
        
        self._gpin_ptt.setChecked(a_chunk.in_ptt != 0)
        self._gpin_b.setChecked(a_chunk.in_b != 0)
        self._gpin_c.setChecked(a_chunk.in_c != 0)
        self._gpin_d.setChecked(a_chunk.in_d != 0)
        self._gpin_sw2.setChecked(a_chunk.in_sw2 != 0)

    # ------------------------------------------------------
    def _parse_res_datetime(self, a_chunk : Wira_res_datetime ):
        self._println("Have datetime "+str(a_chunk))
        self._wiradio_date_time.set(str(a_chunk))
        
                
    # ------------------------------------------------------
    def _wira_poll_periodic(self):

        pass
        
    
    
    
# ======================================================================
# this should receive a response packet and provide an iterator into the whole list of chunks
class Wipp_parse_packet():        

    # --------------------------------------------------------------------
    # this will read a packet and provide an iterator
    def __init__(self, has_println : HasPrintln ):
        self._has_println = has_println

    # -------------------------------------------------------------------------------
    def read_packet(self, a_socket : WisocketTcp ):
        h_two_bytes : bytes = a_socket.socket_rcvall(2)

        if h_two_bytes:
            a_touple = struct.unpack('<H',h_two_bytes)
            self._pack_len = a_touple[0]
        else:
            self._pack_len =0
            
        if self._pack_len < 2:
            self._println('missing packet len '+str(self._pack_len))
            return
        
        # remaining bytes are two less, the packet len
        remaining_bytes = a_socket.socket_rcvall(self._pack_len-2)
        
        #self._println("read_packet pack_len="+str(self._pack_len))
        
        # I keep just the chunks buffer
        self._iterator = IppChunkIterator(bytearray(remaining_bytes))

    # -------------------------------------------------------------------------------
    def _println(self, msg : str ):
        self._has_println._println(msg)
        
        
# ===================================================================
# if you need to handle some special parser for receiving responses
# you need to override this and define your own implementation        
class Wira_pull_file_parser(Wipp_parse_packet):        
    
    # --------------------------------------------------------------------
    # the init just pick up the println, all is done in the read_packet
    # I also need to know the filename, so I can write the same here
    def __init__(self, o_parent : Wiradio_gui, file_name : str ):
        Wipp_parse_packet.__init__(self, o_parent)
        
        self._parent = o_parent
        self._file_name : str = file_name

    # ------------------------------------------------------
    # parse a file pull
    # @return true if there is eof
    def _parse_res_file_pull(self, a_chunk : Wira_res_file_pull, to_file ) -> bool:
        #self._println("_parse_res_file_pull "+str(a_chunk))
        
        if not to_file is None:
            to_file.write(a_chunk.getFilePart()) 
        
        return a_chunk.isEof()


    # ------------------------------------------------------
    # parse a received chunk
    # @return True if an EOF is found
    def _parse_chunk(self, a_chunk : IppChunk, to_file ) -> bool:

        if a_chunk is None: 
            self._println("Wira_pull_file_parser._parse_chunk NULL")
            return True
      
        chunk_id = a_chunk.chunk_id
        
        if chunk_id == IPPW_res_filechunk:
            return self._parse_res_file_pull(Wira_res_file_pull(a_chunk), to_file)
        elif chunk_id == IPPW_res_generic:
            # res generic is an error message
            res_generic = Wira_res_generic(a_chunk)
            self._println('_parse_chunk '+str(res_generic))
            return True
        else:
            self._println("Wira_pull_file_parser._parse_chunk(a): FAIL unsupported id "+str(chunk_id))
            # on mismatch assume Eof
            return True


    # -------------------------------------------------------------------------------
    # this read one of many packets, needed to transfer files keeping control of what is happening
    # @return True if it finds an EOF condition
    def _read_packet_one(self, a_socket : WisocketTcp, to_file ) -> bool:
        
        try:
            a_reader = Wipp_parse_packet(self._has_println)
            a_reader.read_packet(a_socket)
            
            for a_chunk in a_reader._iterator:
                #self._println('read_packet_one '+str(a_chunk))
                if self._parse_chunk(a_chunk, to_file):
                    return True
                
            return False
            
        except Exception as _xc:
            self._println('_read_packet_one exception '+str(_xc))
            return True

    # -------------------------------------------------------------------------------
    def read_packet(self, a_socket : WisocketTcp ):
        
        self._println("read_packet: pull_file")
        
        mlang_top : pathlib.Path = self._parent.stat.glob_mlang.getMlangTopDir()
        
        if not mlang_top:
            self._println('read_packet EMPTY destination dir')
            return
        
        to_file_name = mlang_top / self._file_name
        self._println('writing to '+str(to_file_name))
        
        with open(to_file_name, 'wb') as tofile:
                   
            while not self._read_packet_one(a_socket, tofile):
                pass
        
        self._println('receive file END')
        




# ===================================================================
# This will push a file to wiradio
# Note that the push request is already sent and this is parsing the request result        
class Wira_push_file_parser(Wipp_parse_packet):        
    
    # --------------------------------------------------------------------
    # the init just pick up the println, all is done in the read_packet
    # I also need to know the filename, so I can write the same here
    def __init__(self, o_parent : Wiradio_gui, from_file_name : pathlib.Path ):
        Wipp_parse_packet.__init__(self, o_parent)
        
        self._parent = o_parent
        self._from_file_name = from_file_name

    # ------------------------------------------------------
    # parse a received chunk
    # @return True if an EOF is found
    def _parse_chunk(self, a_chunk ) -> bool:

        if a_chunk is None: 
            self._println("Wira_push_file_parser._parse_chunk NULL")
            return True
      
        chunk_id = a_chunk.chunk_id
        
        if chunk_id == IPPW_res_generic:
            # res generic can report an error or a continue operation
            res_generic = Wira_res_generic(a_chunk)
            self._println('Wira_push_file_parser._parse_chunk '+str(res_generic))
            return False
        else:
            self._println("Wira_push_file_parser._parse_chunk(a): FAIL unsupported id "+str(chunk_id))
            # on mismatch assume Eof
            return True


    # -------------------------------------------------------------------------------
    # this read one of many packets, needed to transfer files keeping control of what is happening
    # @return True if it finds an EOF condition
    def _read_packet_one(self, a_socket : WisocketTcp ) -> bool:
        
        try:
            a_reader = Wipp_parse_packet(self._has_println)
            a_reader.read_packet(a_socket)
            
            for a_chunk in a_reader._iterator:
                #self._println('read_packet_one '+str(a_chunk))
                if self._parse_chunk(a_chunk):
                    return True
                
            return False
            
        except Exception as _xc:
            self._println('_read_packet_one exception '+str(_xc))
            return True

    # -------------------------------------------------------------------------------
    # this should break out the file into chunks....
    # return True if I have to continue sending pieces
    # when sending fc_count MUST be > fc_index, IN THE CHUNK
    def _write_packet_one(self, a_socket : WisocketTcp, fromfile : io.BufferedReader ) -> bool:
        
        try:
            chu_bytes = fromfile.read(IPPW_WFS_FCHU_MAXLEN)
            
            if chu_bytes:
                self._fc_count += 1
                
            # now, I pick up the current index and the possibly incremented fc_count
            a_chunk = Wira_req_file_chunk(self._fc_index, self._fc_count, chu_bytes)

            if chu_bytes:
                self._fc_index += 1
            
            # create a request
            a_req = Wira_request("Write file packet")
            
            # add a chunk
            a_req.add_chunk(a_chunk)
            
            # send it off (there is no reply)
            a_req.send_request(a_socket)
            
            #self._println(' _write_packet_one fc_index '+str(self._fc_index))
            
            if chu_bytes:
                return True
            else:
                return False
        except Exception as _exc :
            self._println('   _write_packet_one _exc '+str(_exc))
            return False            

    # -------------------------------------------------------------------------------
    # the request should come back with a confirmation to be able to receive the file
    # then, I can send the shole file and that is it....
    def read_packet(self, a_socket : WisocketTcp ):
        
        self._println("read_packet: push file")
       
        # here I should attempt to open the file for reading, cannot do this in the GUI
        self._println('   reading from '+str(self._from_file_name))
        
        # here I should wait for ACK, I may receive EOF
        if self._read_packet_one(a_socket):
            return
        
        self._fc_index=0
        self._fc_count=0
        
        # Wiradio has told me OK to send a file, what I should do next is to send a sequence of packets
        with open(self._from_file_name, 'rb') as fromfile:
                   
            while self._write_packet_one(a_socket, fromfile):
                pass
        
        self._println('push file END')
        
        
# ====================================================================
# this thread is started on GUI request
# it will show what is "on air" and then attempt to bind to Wiradio
        
class Wibtle_thread (Thread, HasPrintln):

    # ----------------------------------------------------------------
    def __init__(self, parent : Wiradio_gui ):
        Thread.__init__(self,name="Wi BTLE",daemon=True)
        
        self._parent = parent
        
        self._commands_queue : queue.Queue[str] = queue.Queue()
        
    # ----------------------------------------------------------------
    # the client will wait on this queue for commands to send
    def queue_command (self, a_command : str ):
        self._commands_queue.put_nowait(a_command)
        
    # ----------------------------------------------------------------
    def _println(self, msg : str ):
        self._parent._println(msg)
        
    # ----------------------------------------------------------------
    def _set_BT_status_label(self, msg : str ):
        self._parent.set_BT_status_label(msg)        
        self._println(msg)
        
    # ----------------------------------------------------------------
    def run(self):
        self._set_BT_status_label ("START "+self.name)

        try:
            asyncio.run(self._wible_scanner_callback_mode())
            asyncio.run(self._wible_wiradio_client())
        except Exception as _err:
            self._println("wiradio_gui.run() main_bt ERROR "+str(_err))

    # ------------------------------------------------------------
    # called when the scanner receive something, should trigger stop on third call
    def _btle_scanner_callback(self, device, advertising_data):
        
        self._println("_btle_scanner_callback "+str(advertising_data))
        
        self._callback_count += 1
        
        if self._callback_count > 3:
            self._scanner_stop_event.set()

    # ---------------------------------------------------------------
    # this just scan the air in callback mode and will exit on third callback
    # used to find out if there is something on air
    async def _wible_scanner_callback_mode(self):
        
        self._scanner_stop_event = asyncio.Event()
        self._callback_count=0
    
        async with BleakScanner(self._btle_scanner_callback) as scanner:
            self._println("scanner backend="+str(scanner._backend))
            # Wait for an event to trigger stop, otherwise scanner will stop immediately.
            await self._scanner_stop_event.wait()
    
        self._println('_wible_scanner_callback_mode END')
        
    # ----------------------------------------------------------------------
    # ecample of a scanner that waits some time for data, not currently used
    async def main_scanner_waiter(self):
        devices = await BleakScanner.discover(timeout=5, return_adv=True)
        for key, value in devices.items():
            self._set_BT_status_label ("BT discovered "+str(key))
            self._wible_scanner_show_single(value)

    # ------------------------------------------------------
    # this gets an arry of two element, the first one is a BLEDevice and the second one
    # is a dictionary holding advertisement data
    def _wible_scanner_show_single(self, tupole  ):
        
        a_dev = tupole[0]
        self._println ("  [0] "+str(a_dev) )
        
        adv_data = tupole[1]
        self._wible_scanner_show_adv_data(adv_data)
        #self._println ("  [1] "+str(adv_data) )
    
    
    # ------------------------------------------------------
    # this gets an arry of two element, the first one is a BLEDevice and the second one
    # is a dictionary holding advertisement data
    def _wible_scanner_show_adv_data(self, adv_data : AdvertisementData ):
        
        s_data = adv_data.service_data
        for key,value in s_data.items():
            self._println ("  k="+str(key)+' v='+str(value) )
            
        s_uuid = adv_data.service_uuids
        for a_uu in s_uuid:
            self._println ("  u="+str(a_uu))





    # -----------------------------------------------------------------------------
    # you should get the data in the io queue
    async def _wible_wiradio_client(self):
        self._println ("starting scan...")
    
        device = await BleakScanner.find_device_by_name('Wiradio', cb={"use_bdaddr": False}  )
        if device is None:
            self._println ("could not find device with name Wiradio")
            raise BleakDeviceNotFoundError('Wiradio')
    
        self._println ("connecting to device Wiradio")

        # ------------------------------------------------------------------------
        # method inside a method that handles the disconnect
        def _wible_handle_disconnect(_a_client : BleakClient):
            self._println("Device was disconnected, goodbye.")
            # cancelling all tasks effectively ends the program
            #for task in asyncio.all_tasks():
            #    task.cancel()

        # -----------------------------------------------------------------------------
        # method inside a method that handles the callback for data in a characteristic
        def _client_callback_handler(_spp_chara : BleakGATTCharacteristic, data: bytearray) -> None:
            if data:
                a_string = data.decode() 
                self._println ("message from wiradio: "+a_string)
            else:
                self._println ("message from wiradio: NULL")
    
        # this is NOT a method, it is the actual body of the origianl method
        async with BleakClient(device, disconnected_callback=_wible_handle_disconnect) as client:
            self._set_BT_status_label ("connected")
            
            nus = client.services.get_service(UART_SERVICE_UUID)
            self._println (" nus="+str(nus))

            if nus is None:
                raise BleakDeviceNotFoundError("UART service not found")
            
            spp_chara = nus.get_characteristic(UART_RX_CHAR_UUID)
            self._println (" spp_chara="+str(spp_chara))

            if spp_chara is None:
                raise BleakDeviceNotFoundError("CHARACTERISRIC not found")
            
            # bind notifications for this characteristics
            await client.start_notify(spp_chara, _client_callback_handler)

            loop = asyncio.get_running_loop()

            while True:
                # this will allow the get from the queue while still having the asyncio running
                a_command = await loop.run_in_executor(None, self._commands_queue.get)

                if not a_command:
                    break
                
                # I wish to write something to this characteristics
                await client.write_gatt_char(spp_chara, a_command.encode(), response=True)
                self._println ('wibtle sent ['+str(a_command)+']');

            self._set_BT_status_label ("client.stop_notify")
            
            await client.stop_notify(spp_chara)
    
        self._set_BT_status_label ("disconnected")
            
        
        
        
        
        
        
        
        
        
        
        
        
        
        

