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

'''

from __future__ import annotations

import csv
from threading import Thread
import traceback
from typing import List, Protocol, Iterable, Any
import typing

import qpystat


# this just defines an alias for a type
Csv_Reader = typing.Iterator[Any]

csv_th_marker='prepperdock_th'



# =============================================================
# load and save of csv need the list of tables to load/save
# channels, radio fm, contacts, generic params
# so, it is something similar to app_config
class AppCsv():
    
    # ----------------------------------------------------------------------
    def __init__(self, stat : qpystat.Qpystat):
        self.stat = stat

        # it is a list of csv tables to be saved
        self.tables_list : List[CsvTable] = []

    # ------------------------------------------------------------------------
    def _println(self, msg : str):
        self.stat.app_println(msg)

    # ------------------------------------------------------------------------
    # instances can call this one if they wish to be export inport CSV
    def add_to_tablesList(self, atable : CsvTable ):
        self.tables_list.append(atable)

    # ----------------------------------------------------------------------
    # Call this from swing thread
    def export_to_file(self, file_name : str ):
        e_thread=CsvExportThread(self.stat, self, file_name)
        e_thread.start()

    # ----------------------------------------------------------------------
    # Call this from swing thread
    def import_from_file(self, file_name : str ):
        e_thread=CsvImportThread(self.stat, self, file_name)
        e_thread.start()
        
    # ----------------------------------------------------------------------
    def getCsvTableUsingName(self, t_name : str ) -> CsvTable:
        
        for row in self.tables_list:
            if row.csv_tableName() == t_name:
                return row
            
        return typing.cast(CsvTable, None)


# ==========================================================================
# It needs to be detached from GUI
class CsvImportThread(Thread):
    
    st_table='st_table'
    st_header='st_header'
    st_rows='st_rows'
    
    # ----------------------------------------------------------------------
    # Constructor
    def __init__(self, stat : qpystat.Qpystat, parent : AppCsv, file_name : str ):
        Thread.__init__(self,name="CSV Import Thread")

        self.stat=stat
        self.parent=parent
        self.file_name=file_name
        self._p_state = self.st_table
        self._cur_table : CsvTable

    # ----------------------------------------------------------------------
    # https://docs.python.org/3/library/csv.html
    
    # ----------------------------------------------------------------------
    def _println(self, msg : str):
        self.stat.app_println(msg)


    # ----------------------------------------------------------------------
    def _parse_st_table(self, row : typing.List[str]):
        if not row:
            self._println('_parse_st_table: EMPTY row')
            return
        
        h_marker = row[0]
        w_tablename= row[1]
        
        if h_marker == csv_th_marker:
            self._cur_table = self.parent.getCsvTableUsingName(w_tablename)
            if self._cur_table:
                self._println('_parse_st_table: table '+str(self._cur_table.csv_tableName()))
                self._p_state=self.st_header
            else:
                self._println('_parse_st_table: INVALID table '+str(w_tablename))
        else:
            self._println('_parse_st_table: INVALID row '+str(row))
        

    # ----------------------------------------------------------------------
    # In theory I should pass the header to the table to arrange the remapping
    def _parse_st_header(self, row : typing.List[str]):

        if self._cur_table is None:
            self._println('_parse_st_header: NO table '+str(row))
            # go back to searching tables
            self._p_state=self.st_table
        else:
            self._cur_table.csv_parse_header(row)
            # at the moment I just change state
            self._p_state=self.st_rows
        
    # ----------------------------------------------------------------------
    # parsing a row is in trycatch so I can find out what happens
    def _parse_st_row_try(self, row_values : typing.List[str]):
        try:
            self._cur_table.csv_parse_row(row_values)
        except Exception as _err:
            self._println("INVALID "+str(row_values))
            self._println(traceback.format_exc())
        
    # ----------------------------------------------------------------------
    # In theory I should pass the header to the table to arrange the remapping
    # at the moment I assume it is like a paste from clipboard
    def _parse_st_row(self, row_values : typing.List[str]):
        
        if len(row_values) < 1:
            self._p_state=self.st_table
            self._println('_parse_st_rows: table end')
        elif self._cur_table is None:
            self._println('_parse_st_rows: NO table '+str(row_values))
        else:
            self._parse_st_row_try(row_values)


    # ----------------------------------------------------------------------
    def _parse_row(self, row_values : typing.List[str] ):
        if self._p_state == self.st_table:
            self._parse_st_table(row_values)
        elif self._p_state == self.st_header:
            self._parse_st_header(row_values)
        elif self._p_state == self.st_rows:
            self._parse_st_row(row_values)
        else:
            self._println('_parse_row: INVALID state '+str(self._p_state)+' '+str(row_values))
            

    # ----------------------------------------------------------------------
    # the first row should have a heading and a table name 
    # then you call parse on the given table name
    # note that a row is really a list of string !
    def csv_parse(self, csv_file ):
        
        c_reader : Csv_Reader = csv.reader(csv_file, delimiter='\t', escapechar='\\', quotechar='"', quoting=csv.QUOTE_MINIMAL)
        
        for row in c_reader:
            self._parse_row(row)

 
    # ----------------------------------------------------------------------
    def run(self):
        self._println('CSV Import start '+self.file_name)

        if not self.file_name:
            self._println('missing file_name CSV Import END')
            return
        
        try:
            # need to open with newline='' to avoid double newlines
            with open(self.file_name, 'r', newline='') as csv_file:
                self.csv_parse(csv_file)
        except Exception as exc :
            self._println("CsvImportThread "+str(exc))
            
        self.stat.eeprom_gui.GUI_refresh()    
        
        self._println('CSV Import END')































# ==========================================================================
# It needs to be detached from GUI
class CsvExportThread(Thread):
    
    
    # ----------------------------------------------------------------------
    # Constructor
    def __init__(self, stat : qpystat.Qpystat, parent : AppCsv, file_name : str ):
        Thread.__init__(self,name="CSV Export Thread")

        self.stat=stat
        self.parent=parent
        self.file_name=file_name

    # ----------------------------------------------------------------------
    # https://docs.python.org/3/library/csv.html
    
    def _println(self, msg : str):
        self.stat.app_println(msg)


    # ----------------------------------------------------------------------
    def write_tables(self, csv_file ):
        
        c_writer : Csv_Writer = csv.writer(csv_file, delimiter='\t', escapechar='\\', quotechar='"', quoting=csv.QUOTE_MINIMAL)
        
        for table in self.parent.tables_list:
            theader=(csv_th_marker,table.csv_tableName())
            c_writer.writerow(theader)
            table.csv_write_header(c_writer)
            table.csv_write_table(c_writer)
            c_writer.writerow([])

    # ----------------------------------------------------------------------
    def run(self):
        self._println('CSV Export start '+self.file_name)

        if not self.file_name:
            self._println('missing file_name CSV Export END')
            return
        
        try:
            # need to open with newline='' to avoid double newlines
            with open(self.file_name, 'w', newline='') as csv_file:
                self.write_tables(csv_file)
        except Exception as _exc :
            self._println(traceback.format_exc())
        
        self._println('CSV Export END')
        
    
    
# ==============================================================
# this is the MOST idiotic thing of python that I can immagine
# the STANDARD module csv does NOT expose the generic class Writer
# SO, one has to "guess" the API, kind of "cast" the api into ANOTHER class to have some decent type support
# IDIOTS reinventing the wheel as a line    
class Csv_Writer(Protocol):
    def writerow(self, row: Iterable[Any]) -> Any:
        ...

    def writerows(self, rows: Iterable[Iterable[Any]]) -> None:
        ...




# =============================================================
# object that want to be savable must implement this one
class CsvTable():
    
    # ----------------------------------------------------------
    def __init__(self):
        pass
    
    # ----------------------------------------------------------
    # MUST override this method
    def csv_tableName(self) -> str:
        return 'empty_table_name'
    
    # ----------------------------------------------------------
    # write the table header
    def csv_write_header(self, c_writer : Csv_Writer ):
        pass
    
    # ---------------------------------------------------------
    # in a different thread than swing, write the table to this csv writer
    def csv_write_table(self, c_writer : Csv_Writer ):
        pass
    
    # ---------------------------------------------------------
    # this will be called when it is time to parse the header for this table    
    def csv_parse_header(self, row : typing.List[str] ):
        pass
    
    # ---------------------------------------------------------
    # this will be called when it is time to parse the data row for this table    
    # what is passed is a row of data as written in the CSV file
    def csv_parse_row(self, row_values : typing.List[str] ):
        pass
        
        



