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

'''
# -------------------------------------------------------------------------
# There is only ONE poll, this is a GUI and if there is something to send it should be set to the poll

from __future__ import annotations

from tkinter import  IntVar, ttk
import typing

from app_config import AppConfig, ConfigSavable
from glob_eeprom import EEblock
from glob_gui import JtkWinToplevel, TV_Entry, GUI_hide_show_window, \
    JtkLabel, JtkPanelGrid, JtkPanelPackTop, TV_EntryFrequency_MHz_1x 
from glob_ippqs import Qsk5_req_read_eeprom, Qsk5_res_Loudest
import glob_ippqs
from glob_mlang import MlangLabel
from ipp_parser import Qsk5_req_scan_loud, Qsk5_req_syscmd, IPPU_SYSCMD_reset
import qpystat


QSCALIB_EE_ADDRESS=0x1F80   # NOTE that I load a full block !
QSSCANF_DEFAULT=44610625
QSALLOWF_DELTA=20

# ====================================================================
class Qscalibrate_gui (ConfigSavable,GUI_hide_show_window):

    # ----------------------------------------------------------------
    def __init__(self, stat : qpystat.Qpystat):
        self._stat = stat
        
        stat.appconfig.addMyselfToSavables(self)

        self._toplevel = JtkWinToplevel("Calibrate window")
        
        a_panel=self._newCenterPanel(self._toplevel)

        a_panel.pack(fill='both', expand=True)
        
        self.GUI_hide_window() 
        
        self._println("Messagy init complete \u25C4")

    # --------------------------------------------------------------------
    # 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()    

    # -------------------------------------------------------------------
    # prints something on the application log
    def _println(self, msg):
        self._stat.app_println(msg)    


    # -------------------------------------------------------------------------------
    def _newDataPanel(self, parent) -> ttk.Frame:

        a_panel = JtkPanelGrid(parent)

        a_panel.addItem(JtkLabel(a_panel,"Step 1",style="Bold.TLabel"))
        a_panel.addItem(ttk.Button(a_panel,text="Read Calibration" , command=self._click_readEeprom ))
        a_panel.nextRow()
        
        self._edit_AF_rx_gain2 = IntVar(a_panel)
        a_panel.addItem(JtkLabel(a_panel,"AF rx gain2 (def. 44) "))
        a_panel.addItem(TV_Entry(a_panel, self._edit_AF_rx_gain2, width=4))
        a_panel.addItem(JtkLabel(a_panel,"optional audio parameter"))
        a_panel.nextRow()

        self._edit_AF_dac_gain = IntVar(a_panel)
        a_panel.addItem(JtkLabel(a_panel,"AF dac gain (def. 14) "))
        a_panel.addItem(TV_Entry(a_panel, self._edit_AF_dac_gain, width=4))
        a_panel.addItem(JtkLabel(a_panel,"optional audio parameter"))
        a_panel.nextRow()

        self._edit_xtal_adjust = IntVar(a_panel)
        a_panel.addItem(JtkLabel(a_panel,"Frequency adjust "))
        a_panel.addItem(TV_Entry(a_panel, self._edit_xtal_adjust, width=4))
        a_panel.nextRow()

        self._scan_want_frequency = TV_EntryFrequency_MHz_1x(a_panel,value=QSSCANF_DEFAULT)
        a_panel.addItem(JtkLabel(a_panel,"Want Frequency "))
        a_panel.addItem(self._scan_want_frequency)
        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,"Step 2",style="Bold.TLabel"))
        a_panel.addItem(ttk.Button(a_panel,text="Start Frequency sample" , command=self._click_F_sampler ))
        a_panel.nextRow()

        self._F_statistics = FrequencyStatistics()
        a_panel.addItem(self._F_statistics.newGui(a_panel),columnspan=2)
        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,"Step 3",style="Bold.TLabel"))
        self._writeButton=ttk.Button(a_panel,text="Write" , command=self._click_writeEeprom, state='disabled')
        a_panel.addItem(self._writeButton)

        return a_panel


    # -------------------------------------------------------------------------------
    def _newCenterPanel(self, parent) -> ttk.Frame:

        a_panel = JtkPanelPackTop(parent)

        a_panel.addItem(MlangLabel(a_panel,"Radio Calibration",self._stat.glob_mlang, style='Header1.TLabel' ))
        a_panel.addItem(JtkLabel(a_panel,"No difference means that the oscillator is corrent"))
        a_panel.addItem(JtkLabel(a_panel,"Adjust +-1 means an adjustment of +-50Hz"))
        a_panel.addItem(JtkLabel(a_panel,"FRQ wanted MUST be different than current radio channel FRQ"))
        a_panel.addItem(self._newDataPanel(a_panel))
        

        return a_panel
    
    # --------------------------------------------------------------------------------------
    # If not started should start a thread that samples the F strong and shows the result
    # Actually, the results come trough a callback !
    def _click_F_sampler(self):
        self._println("CLICK start F sampler")
        
        # on click start I should clear the stats
        self._F_statistics.clear()

        self._reqScanLoud()

        # let me do just one sample, now


    # --------------------------------------------------------------------------------------
    def _F_want_get(self) -> int:
        try:
            return int(self._scan_want_frequency.get_value())
        except Exception as _exc:
            return QSSCANF_DEFAULT

    # --------------------------------------------------------------------------------------
    # Request a scan loud to radio
    def _reqScanLoud(self):
        a_f = self._F_want_get()
        cmd = Qsk5_req_scan_loud(50,a_f)
        self._stat.qconnect.queuePut(cmd)
    
    
    # --------------------------------------------------------------------------------------
    # this is called when the result for F scan arrives
    # It should continue scanning until the required samples are received
    def receive_scan_loud_res(self, chunk : Qsk5_res_Loudest):
        
        xtal_adjust = self._edit_xtal_adjust.get()
        
        scan_continue = self._F_statistics.addSample(xtal_adjust, self._F_want_get(), chunk.found_F_1x)
        
        if scan_continue:
            self._reqScanLoud()
        else:
            self._println("receive_scan_loud_res: scan END")

    
    
    # --------------------------------------------------------------------------------------
    # this should read the eeprom
    def _click_readEeprom(self):
        self._println("CLICK Read Eeprom Calibrate")

        cmdb = Qsk5_req_read_eeprom(QSCALIB_EE_ADDRESS,1)
        self._stat.qconnect.queuePut(cmdb)

    
    # --------------------------------------------------------------------------------------
    # The block holding the calibration should update values
    def _click_writeEeprom(self):
        self._println("CLICK Write Eeprom")

        a_block : EEblock=self._stat.globeeprom.get_block_from_address(QSCALIB_EE_ADDRESS)

        f_adj=self._edit_xtal_adjust.get()
        rx_gain2=self._edit_AF_rx_gain2.get()
        dac_gain=self._edit_AF_dac_gain.get()
        
        a_block.eeprom_pack_into('<hxxxxBB',8,f_adj,rx_gain2,dac_gain)  
 
        cmd1 = glob_ippqs.Qsk5_req_write_eeprom(a_block.ee_start_a, a_block.block_bytes_len, a_block.ee_bytearray)
        self._stat.qconnect.queuePut(cmd1)
        self._println("calibrate cmd1="+str(cmd1))
        
        cmd2 = Qsk5_req_syscmd(IPPU_SYSCMD_reset)  # this request a radio reset
        self._stat.qconnect.queuePut(cmd2)
        self._println("calibrate cmd2="+str(cmd2))

    
    
    # ------------------------------------------------------------------------
    # when eeprom is loaded I arrive here
    def updateGuiFromEeprom(self):
        '''
        int16_t  xtal_adjust;
        uint16_t EEPROM_1F8A;               // unknown
        uint16_t EEPROM_1F8C;               // unknown
        uint8_t  AF_rx_gain2;
        uint8_t  AF_dac_gain;
        '''
        self._println("EEPROM loaded")
        
        a_block : EEblock=self._stat.globeeprom.get_block_from_address(QSCALIB_EE_ADDRESS)
        
        xtal_adjust,af_rx_gain2,af_dac_gain = a_block.eeprom_unpack('<hxxxxBB',8)  
        
        self._edit_xtal_adjust.set(xtal_adjust)
        self._edit_AF_rx_gain2.set(af_rx_gain2)
        self._edit_AF_dac_gain.set(af_dac_gain)
        
        self._writeButton['state'] = 'normal'

        
        

        
    # -------------------------------------------------------------------
    # implement the config savable
    def appImportConfig(self, cfg : AppConfig ):
        adict = cfg.getDictValue("qscalibrate_cfg", {})
        
        try:
            self._toplevel.setGeometry(adict['gui_cfg'])
        except Exception as _exc :
            pass            

    # -------------------------------------------------------------------
    # implement the config savable
    def appSaveConfig(self, cfg : AppConfig ):
        adict : dict[str,typing.Any] = {}

        adict['gui_cfg'] = self._toplevel.getGeometry()

        cfg.setDictValue("qscalibrate_cfg", adict )            
    
    
# ==========================================================================
# holds the part to handle frequency statistics
# you create the gui usign the appropriate method
class FrequencyStatistics:
    
    # ----------------------------------------------------------------------
    def __init__(self):
        self._sample_count=0
        self._poll_count=0
        self._sample_F_sum_1x=0;

    # ----------------------------------------------------------------------
    # can ONLY be called after GUI is created, in swing thread    
    def clear(self):
        self._sample_count=0
        self._sample_F_sum_1x=0;
        self._poll_count=0
        self._F_avg.set_value(0)
        self._sample_count_label.setText(str(self._sample_count))
    
    # ----------------------------------------------------------------------
    def newGui(self, parent_panel) -> ttk.Frame:
        a_panel = JtkPanelGrid(parent_panel)

        self._F_rx = TV_EntryFrequency_MHz_1x(a_panel)
        a_panel.addItem(JtkLabel(a_panel,"RX Frequency"))
        a_panel.addItem(self._F_rx)
        a_panel.nextRow()

        self._F_avg = TV_EntryFrequency_MHz_1x(a_panel)
        a_panel.addItem(JtkLabel(a_panel,"RX Frequency average"))
        a_panel.addItem(self._F_avg)
        a_panel.nextRow()
        
        a_panel.addItem(JtkLabel(a_panel,"Sample Count"))
        self._sample_count_label = JtkLabel(a_panel,"0")
        a_panel.addItem(self._sample_count_label)
        a_panel.nextRow()

        a_panel.addItem(JtkLabel(a_panel,"Status Message"))
        self._status_label = JtkLabel(a_panel,"Ready",style="Bold.TLabel")
        a_panel.addItem(self._status_label)

        return a_panel
    
    
    # ----------------------------------------------------------------------
    def _show_status(self, msg : str ):
        self._status_label.setText(msg)
    
    # ----------------------------------------------------------------------
    # @return True if the have is whithin range of want
    def _F_in_range(self, F_want_1x : int , F_have_1x : int ) -> bool:
        
        if F_have_1x < F_want_1x - QSALLOWF_DELTA:
            return False
    
        if F_have_1x > F_want_1x + QSALLOWF_DELTA:
            return False
    
        return True
    
    # ----------------------------------------------------------------------
    # add one sampled value
    # @return True if should go on sampling
    def addSample(self, cur_xtal_adjust : int, F_want_1x : int , F_have_1x : int ) -> bool:

        self._poll_count += 1

        self._sample_count_label.setText(str(self._poll_count)+' '+str(self._sample_count))

        self._F_rx.set_value(F_have_1x)
        
        if not self._F_in_range(F_want_1x, F_have_1x):
            self._show_status('F rx out of range')
            return True

        self._sample_F_sum_1x += F_have_1x 
        self._sample_count = self._sample_count + 1 

        # I just incremented by one, so, it is never zero
        F_avg_1x : int = (int)(self._sample_F_sum_1x / self._sample_count)  

        self._F_avg.set_value(F_avg_1x)

        self._sample_count_label.setText(str(self._poll_count)+' '+str(self._sample_count))

        # to have the right "sign" this is the order of subtraction
        F_delta_1x = F_want_1x - F_avg_1x 

        # the delta values are in 1x mode 
        # apparently the adjust is in 50Hz resolution, meaning that every digit is 50Hz
        adjust_delta : int = (int)(F_delta_1x / 5)

        self._show_status('Better adjust is '+str(cur_xtal_adjust+adjust_delta))
       
        # If we are not done acquiring values
        if self._sample_count < 10:
            return True  
        
        return False
        
    