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

AGC, configurable trough prepperdock
  
''' 

from __future__ import annotations

from tkinter import ttk, messagebox, StringVar
import tkinter
import typing

from app_csv import CsvTable, Csv_Writer
from glob_eeprom import GlobEeprom, EEblock
from glob_fun import getResourcesPhotoimage
from glob_gui import EE_events_listener, JtkPanelPackTop, JtkPanelGrid, JtkLabel, \
    JtkPanelPackLeft, JtkButtonImage
import glob_gui
import qseeprom_gui


# =====================================================================
# the table with the list of FM channels
# It should take the data from the global EEPROM repository
# It will be signalled when new EEPROM data is received
class Qsagc_gui (EE_events_listener):

    # ----------------------------------------------------------------
    def __init__(self, a_parent : qseeprom_gui.Qseeprom_gui, parent_frame : ttk.Frame ):
        self._stat = a_parent._stat
        self._log_panel : glob_gui.LogPanel = a_parent._log_gui

        self.work_panel = ttk.Frame(parent_frame)
        
        c_panel=self._newCenterPanel(self.work_panel)
        
        c_panel.pack(fill='both', expand=True)

    # -------------------------------------------------------------------------------
    def _println(self, msg : str ):
        self._log_panel.println(msg)
        
    # -------------------------------------------------------------------------------
    # On center panel there is the table and scroll bar
    # NOTE: The layout of this is described by caller
    # https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/ttk-Treeview.html
    def _newCenterPanel(self, parent):

        a_panel = JtkPanelPackTop( parent) #, borderwidth=3, relief='ridge')
        
        a_panel.addItem(JtkLabel(a_panel,'Automatic Gain Control',style='Header1.TLabel', anchor='center'))
        
        self._qsagc_table = Qsagc_table(self, a_panel)
        a_panel.addItem(self._qsagc_table)

        self._stat.appcsv.add_to_tablesList(self._qsagc_table)

        a_panel.addItem(self._new_buttons_panel(a_panel))
        
        self.bk_registers = StringVar(a_panel)
        a_panel.addItem(ttk.Label(a_panel,textvariable=self.bk_registers))

        a_panel.addItem(JtkLabel(a_panel,'The register order is 3, 2, 1, 0, -1 ..'))

        msg="To avoid serious distortion with high-level input power, \nAGC function is added to automatically adjust the gain of LNA and the gain of VGA"
        a_panel.addItem(JtkLabel(a_panel,msg,style='Header1.TLabel'))

        self.agc_layout=self._new_agc_layout(a_panel)
        a_panel.addItem(self.agc_layout)

        
        return a_panel


    # ------------------------------------------------------------------------------------
    def _new_buttons_panel(self, a_parent : ttk.Widget ) -> ttk.Widget:
        a_panel = JtkPanelPackLeft(a_parent)
        
        a_panel.addItem(ttk.Button(a_panel,text="Beken Default", command=self._click_beken_default ))
        a_panel.addItem(JtkButtonImage(a_panel,getResourcesPhotoimage('save-64x64.png'), command=self._click_save_AGC ))

        return a_panel

    # ------------------------------------------------------------------------------------
    # taken from registers datasheet
    def _click_beken_default(self):
        self._qsagc_table.set_from_list([0x0038,0x25a,0x37b,0x03de,0x0,0x8005,0x00])

    # ------------------------------------------------------------------------------------
    def _click_save_AGC(self):
        self._update_EEblock_from_GUI()

    # ------------------------------------------------------------------------------------
    def _new_agc_layout(self, parent_panel : ttk.Widget ) -> ttk.Widget:
        
        a_img = getResourcesPhotoimage('eeprom-agc-layout.png')
        i_w=a_img.width()
        i_h=a_img.height()
        
        self._println("agc_layout i_w="+str(i_w)+" i_h="+str(i_h))
        
        apanel = ttk.Frame(parent_panel, height=i_h, width=i_w)

        self._canvas = tkinter.Canvas(apanel,height = i_h,width = i_w ) #, bg = 'yellow')
        self._canvas.place(x = 0, y = 0)

        # for reference only, some image zooming
        self.b_img=a_img.zoom(1, 1)
        # now, CENTER the image at i_w and i_h, note that it is NOT the corner, but the image center        
        self._canvas.create_image(i_w/2,i_h/2,image=self.b_img)

        return apanel

    # ------------------------------------------------------------------------
    def tkraise(self):
        self.work_panel.tkraise()
        
    # ------------------------------------------------------------------------
    def showInfo(self, a_message : str ):
        messagebox.showinfo("AGC", a_message, parent=self.work_panel, type='ok')    
        
    # ------------------------------------------------------------------------
    # the RSSI table is quite messy, let me try to use the radio logic...
    def updateGuiFromEeprom(self):
        self._qsagc_table.updateTableFromEeprom(self._stat.globeeprom)
        
        self.GUI_refresh()

    # -----------------------------------------------------------------
    # When this is called, it means the user pressed the write button
    def _update_EEblock_from_GUI(self):

        self._println("qsagc.update_EEblock_from_GUI CALL")

        for row in self._qsagc_table._agc_row_list:
            row.update_EEblock_from_GUI(self._stat.globeeprom)

    # -------------------------------------------------------------------
    # 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 ):    
        pass

        
            

# =================================================================
# I need to store the choices in an object
class Gain_item():
    
    # -------------------------------------------------------------
    # I do not use the bk_value, since it is the index of the selected entry
    # it is here for reference only
    def __init__(self, bk_value : int, gain_dB : int, display_text : str ):
        self._bk_value = bk_value
        self.gain_dB  = gain_dB
        self._display_text=display_text
        
    # --------------------------------------------------
    # this is the equivalent of toString()
    def __str__(self):
        return self._display_text


# =================================================================
# A gain item Is both a value and a combo 
# It happens that the value to write in the BK register is the INDEX !!!
# dB_gain: a number indicating the gain, as from manual, so 0 means a ratio of 1, meaning no gain -9dB means one tenth..
# I need it since I wish to sum the gains in the selected chain of components
# It must support readin from eeprom and writing to eeprom
# NOTE: Since it happens that the index is ALSO the value to write.... it makes things simpler !
class GainItem(ttk.Combobox):

    # ---------------------------------------------------------------
    def __init__(self,  parent_o : Qsagc_row, parent_panel : JtkPanelGrid, gain_choices : list[Gain_item] ):
        self._parent_o=parent_o
        self._gain_choices : list[Gain_item] =gain_choices
        
        s_choices= [str(element) for element in self._gain_choices]
        
        ttk.Combobox.__init__(self, parent_panel,values=s_choices, width=6)
        
        # this is called ONLY when a user changes something !
        self.bind("<<ComboboxSelected>>", self._onComboBoxSelected)

        self._current_choice : Gain_item =self._gain_choices[0]
        self.current(0)
        
    # ------------------------------------------------------------------------------
    # as said before, the Beken value to write is the index of the selected entry
    def getCurrentBkvalue(self):
        return self.current()
        
    # ------------------------------------------------------------------------------
    # the csv value to store is the beken value
    def csv_get_value(self) -> str:
        return str(self.getCurrentBkvalue())
        
    # ------------------------------------------------------------------------------
    # You can set the value from csv using this
    def csv_set_value(self, a_str : str):
        self.setSelectedValue(int(a_str))
        
    # ------------------------------------------------------------------------------
    def getCurrentGainvalue(self):
        return self._current_choice.gain_dB

    # ---------------------------------------------------------------------------------
    # this handles when the combo is selected BY THE USER !!
    def _onComboBoxSelected(self, _event ):
        self._current_choice=self._gain_choices[self.current()]
        # this trigger update on the GUI
        self._parent_o._on_row_changed()
        
    # ---------------------------------------------------
    # this is called BY the application to udate values
    # it will NOT trigger row_changed !
    def setSelectedValue(self, bk_value : int ):
        self.current(bk_value)
        self._current_choice=self._gain_choices[bk_value]
        
    # --------------------------------------------------
    # this is the equivalent of toString()
    def __str__(self):
        return '%02d' % (self.selection_get())

#=====================================================================
# should build content for a Lna short
class GainLnaShort(GainItem):

    def __init__(self, parent_o : Qsagc_row, parent_panel : JtkPanelGrid ):
        
        gains : list[Gain_item] = []
        gains.append(Gain_item(0b00 ,-16 ,'-19dB'))
        gains.append(Gain_item(0b01 ,-16 ,'-16dB'))
        gains.append(Gain_item(0b10 ,-11 ,'-11dB'))
        gains.append(Gain_item(0b11 ,0   ,'0dB'))
        
        GainItem.__init__(self, parent_o, parent_panel, gains)
        

#=====================================================================
# should build content for a Lna normal
class GainLnaNorm(GainItem):

    def __init__(self, parent_o : Qsagc_row, parent_panel : JtkPanelGrid ):
        
        gains : list[Gain_item] = []
        gains.append(Gain_item(0b000 ,-24 ,'-24dB'))
        gains.append(Gain_item(0b001 ,-19 ,'-19dB'))
        gains.append(Gain_item(0b010 ,-14 ,'-14dB'))
        gains.append(Gain_item(0b011 ,-9  ,'-9dB'))
        gains.append(Gain_item(0b100 ,-6  ,'-6dB'))
        gains.append(Gain_item(0b101 ,-4  ,'-4dB'))
        gains.append(Gain_item(0b110 ,-2  ,'-2dB'))
        gains.append(Gain_item(0b111 ,0   ,'0dB'))
        
        GainItem.__init__(self, parent_o, parent_panel, gains)
        

#=====================================================================
# MIXER
class GainMixer(GainItem):

    def __init__(self, parent_o : Qsagc_row, parent_panel : JtkPanelGrid ):
        
        gains : list[Gain_item] = []
        gains.append(Gain_item(0b00 ,-8 ,'-8dB'))
        gains.append(Gain_item(0b01 ,-6 ,'-6dB'))
        gains.append(Gain_item(0b10 ,-3 ,'-3dB'))
        gains.append(Gain_item(0b11 ,0  ,'0dB'))
        
        GainItem.__init__(self, parent_o, parent_panel, gains)


#=====================================================================
# PGA
class GainPga(GainItem):

    def __init__(self, parent_o : Qsagc_row, parent_panel : JtkPanelGrid ):
        
        gains : list[Gain_item] = []
        gains.append(Gain_item(0b000 ,-33 ,'-33dB'))
        gains.append(Gain_item(0b001 ,-27 ,'-27dB'))
        gains.append(Gain_item(0b010 ,-21 ,'-21dB'))
        gains.append(Gain_item(0b011 ,-15 ,'-15dB'))
        gains.append(Gain_item(0b100 ,-9  ,'-9dB'))
        gains.append(Gain_item(0b101 ,-6  ,'-6dB'))
        gains.append(Gain_item(0b110 ,-3  ,'-3dB'))
        gains.append(Gain_item(0b111 ,0   ,'0dB'))
        
        GainItem.__init__(self, parent_o, parent_panel, gains)

    
#=====================================================================
# A gain row should build the row, knowing that the parent is a grid layout
# It assumes it is in a NEW row and WILL add a newxt row at the end    
class Qsagc_row():

    csv_heading=['row_idx',
                 'agc_lnagains',
                 'agc_lnagainn',
                 'agc_mixergain',
                 'agc_pgagain']


    # ---------------------------------------------------------------
    # ee_block
    def __init__(self, parent_o : Qsagc_table, parent_panel : JtkPanelGrid, row_idx : int, row_label : str,  ee_block : int, ee_start : int, r_note : str ):
        self._parent_o=parent_o
        self._ee_block_address=ee_block
        self._ee_offset=ee_start
        
        self.row_idx=row_idx

        idx_label=JtkLabel(parent_panel,row_label)
        parent_panel.addItem(idx_label)
        
        self._lnagains = GainLnaShort(self, parent_panel)
        parent_panel.addItem(self._lnagains)
        
        self._lnagainn = GainLnaNorm(self, parent_panel)
        parent_panel.addItem(self._lnagainn)
        
        self._mixergain = GainMixer(self, parent_panel)
        parent_panel.addItem(self._mixergain)

        self._pgagain = GainPga(self, parent_panel)
        parent_panel.addItem(self._pgagain)
        
        self._total_gain=JtkLabel(parent_panel,style='UMisura.TLabel')
        parent_panel.addItem(self._total_gain)

        note_label=JtkLabel(parent_panel,str(r_note))
        parent_panel.addItem(note_label)

        parent_panel.nextRow()

    # -------------------------------------------------------------------
    def _calc_total_gain(self):

        risul=self._lnagains.getCurrentGainvalue()
        risul=risul + self._lnagainn.getCurrentGainvalue()
        risul=risul + self._mixergain.getCurrentGainvalue()
        risul=risul + self._pgagain.getCurrentGainvalue()
        
        return risul

    # -------------------------------------------------------------------
    # When a combo is changed, I should arrive here
    def _on_row_changed(self):
        self._total_gain.setText('%ddB' % (self._calc_total_gain()))
        self._parent_o._on_table_values_updated()

    # -----------------------------------------------------------------
    # must return the values as a list of strings in the same order as the header
    def csv_get_values(self) -> typing.List[str]:
        
        risul : typing.List[str] = []
        
        risul.append(str(self.row_idx))
        
        risul.append(str(self._lnagains.csv_get_value()))
        risul.append(str(self._lnagainn.csv_get_value()))
        risul.append(str(self._mixergain.csv_get_value()))
        risul.append(str(self._pgagain.csv_get_value()))
        
        return risul
        
        
    # ------------------------------------------------------------------
    # this assumes tha values are in the SAME order and same format as the export !
    # note that s_split is an iterator for strings and the FIRST item (the row index) has already been taken out
    def csv_parse_row(self, r_iter ):
        if not r_iter:
            return 
        
        self._lnagains.csv_set_value(next(r_iter))
        self._lnagainn.csv_set_value(next(r_iter))
        self._mixergain.csv_set_value(next(r_iter))
        self._pgagain.csv_set_value(next(r_iter))

    # ------------------------------------------------------------------
    # If you have a register value, you can use this one
    def updateRowFromRegisterValue(self, a_short : int ):

        self._lnagains.setSelectedValue(  (a_short >> 8) & 0b11  )
        self._lnagainn.setSelectedValue(  (a_short >> 5) & 0b111 )
        self._mixergain.setSelectedValue( (a_short >> 3) & 0b11  ) 
        self._pgagain.setSelectedValue(          a_short & 0b111 )
        
        self._on_row_changed()

    # ------------------------------------------------------------------
    # values are spread in different bloks with different start address        
    def updateRowFromEeprom(self, g_eeprom : GlobEeprom ):
        
        a_block : EEblock = g_eeprom.get_block_from_address(self._ee_block_address)
        a_short=a_block.get_uint16(self._ee_offset)
        
        self.updateRowFromRegisterValue(a_short)        

    # -----------------------------------------------------------------
    def _get_register_bits(self, gain_item : GainItem, shift_bits : int, bit_mask : int ):
        i_val : int = gain_item.getCurrentBkvalue()
        return (i_val & bit_mask) << shift_bits
        
        
    # -----------------------------------------------------------------
    # Need to start packing FROM the top
    def _calc_register_value(self) -> int:
        a_short = 0
        
        a_short = a_short | self._get_register_bits(self._lnagains ,8,0b11)
        a_short = a_short | self._get_register_bits(self._lnagainn ,5,0b111)
        a_short = a_short | self._get_register_bits(self._mixergain,3,0b11)
        a_short = a_short | self._get_register_bits(self._pgagain  ,0,0b111)
        
        return a_short
                    
    # -----------------------------------------------------------------
    # Note that what can be changed are just the values, 
    def update_EEblock_from_GUI(self, g_eeprom : GlobEeprom ):
        a_short=self._calc_register_value()
        a_block : EEblock = g_eeprom.get_block_from_address(self._ee_block_address)
        a_block.set_uint16(self._ee_offset, a_short)

# ===================================================================
# The full table, it could possibly be CFG a and CFG B    
class Qsagc_table(JtkPanelPackTop, CsvTable):  
    
    # -----------------------------------------------------------------
    def __init__(self, parent_o , parent_panel : JtkPanelPackTop ):
        self._parent_o = parent_o
        
        JtkPanelPackTop.__init__(self, parent_panel)
        
        a_panel : JtkPanelGrid = JtkPanelGrid(self)
        
        a_panel.addItem(JtkLabel(a_panel,'Nr.'))
        a_panel.addItem(JtkLabel(a_panel,'LNA short')) 
        a_panel.addItem(JtkLabel(a_panel,'LNA '))
        a_panel.addItem(JtkLabel(a_panel,'MIXER'))
        a_panel.addItem(JtkLabel(a_panel,'VGA(PGA)'))
        a_panel.addItem(JtkLabel(a_panel,'Total'))
        a_panel.addItem(JtkLabel(a_panel,'Note'))
        a_panel.nextRow()
        
        self._agc_row_list : list[Qsagc_row] = []
        self._agc_row_list.append(Qsagc_row(self, a_panel ,0 ,'0'  ,0x1AF0, 2  ,'Strong signal, mute more'))
        self._agc_row_list.append(Qsagc_row(self, a_panel ,1 ,'1'  ,0x1AF0, 4  ,'Average radio signal'))
        self._agc_row_list.append(Qsagc_row(self, a_panel ,2 ,'2'  ,0x1AF0, 6  ,'Louder but still weak'))
        self._agc_row_list.append(Qsagc_row(self, a_panel ,3 ,'3'  ,0x1AF0, 8  ,'For smallest signal, minimal mute'))
        self._agc_row_list.append(Qsagc_row(self, a_panel ,4 ,'-1' ,0x1AF0, 10 ,'Strongest signal, MAX mute (agc_idx=7)'))
        self._agc_row_list.append(Qsagc_row(self, a_panel ,5 ,'-2' ,0x1AF0, 12 ,'Do NOT CHANGE: (agc_idx=6)'))
        self._agc_row_list.append(Qsagc_row(self, a_panel ,6 ,'-3' ,0x1AF0, 14 ,'Do NOT CHANGE: (agc_idx=5)'))
        
        self.addItem(a_panel)

    # ----------------------------------------------------------------------
    def _println(self, msg : str ):
        self._parent_o._println(msg)
                
    # ----------------------------------------------------------------------
    # set the content from list
    def set_from_list(self, values_list : list[int]):
        for index,arow in enumerate(self._agc_row_list):
            a_uint16 = values_list[index]
            arow.updateRowFromRegisterValue(a_uint16)
                
    # -----------------------------------------------------------------------
    # if idx is out of range, return None
    def get_row_using_idx(self, idx : int ) -> Qsagc_row:
        return self._agc_row_list[idx]
        
    # -----------------------------------------------------------------------
    def updateTableFromEeprom(self, g_eeprom : GlobEeprom ):
        for row in self._agc_row_list:
            row.updateRowFromEeprom(g_eeprom)
        
    # ----------------------------------------------------------
    # override CSV
    def csv_tableName(self) -> str:
        return 'radio_agc'
    
    # ----------------------------------------------------------
    # override CSV
    def csv_write_header(self, a_writer : Csv_Writer):
        a_writer.writerow(Qsagc_row.csv_heading)

    # ---------------------------------------------------------
    # in a different thread than swing, write the table to this csv writer
    def csv_write_table(self, a_writer : Csv_Writer):
        
        for row in self._agc_row_list:
            v_list = row.csv_get_values()
            a_writer.writerow(v_list)

    # -------------------------------------------------------------
    # the difference is that clipboard will insert from a given point
    # for CSV, instead, I am given the row index            
    def csv_parse_row(self, row_values : typing.List[str] ):
        r_iter = iter(row_values)
        
        r_idx = int(next(r_iter))
        
        a_row : Qsagc_row = self._agc_row_list[r_idx]
        
        a_row.csv_parse_row(r_iter)

    # ----------------------------------------------------------------
    # get a string of the current register values
    def get_registers_value(self) -> str:
        
        risul = ''
        
        for row in self._agc_row_list:
            reg_val = row._calc_register_value()
            risul = risul + '0x%x ' % (reg_val)
        
        return risul
    
    # ------------------------------------------------------------------
    # called by sub components when values are updated
    # you MUST NOT update the table values from here !!!!!!
    # you will make a circular update !!!!
    def _on_table_values_updated(self):
        r_values=self.get_registers_value()
        self._println('qsagc '+r_values)  
        self._parent_o.bk_registers.set(r_values)
    