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

classes that have a general usage but are not tied to gui can go here

'''
# =======================================================================
# I need a holder for a tcpv4 address that will call a list of function when the apply button is pressed
# so, objects that are interested into the "apply" can register here
# note that on creation you can pass an ip address as string
import ipaddress
import socket
import struct
from tkinter import ttk, StringVar
import typing

import glob_ippqs


# ===================================================================================
# a class can implement this to provide a println
class HasPrintln():
    
    def _println(self, msg : str ):
        pass


# ===================================================================================
# Basic class, this is a response chunk
class IppChunk:
    
    # -------------------------------------------------------------------------------
    # NOTE that the rxbuffer is the whole buffer, including the initial chunk bytes
    def __init__(self ,rxbuffer : bytes, chunk_id=0 ,chunk_len=0):
        self.rxbuffer=rxbuffer
        
        if chunk_id == 0:
            chunk_id = self.rxbuffer[0]
        elif chunk_id != self.rxbuffer[0]:
            raise Exception('chunk_id mismatch','chunk_id mismatch')

        self.chunk_id=chunk_id
        
        if chunk_id & 1 == 0:
            _chunk_id,a_chunk_len=struct.unpack_from('<BB',self.rxbuffer,0)
        else:
            _chunk_id,a_chunk_len=struct.unpack_from('<BxH',self.rxbuffer,0)
            
        if a_chunk_len < 1:
            raise Exception('a_chunk_len is zero','a_chunk_len is zero')

        if chunk_len == 0: 
            self.chunk_len=a_chunk_len
        elif chunk_len != a_chunk_len: 
            raise Exception('a_chunk_len mismatch','a_chunk_len mismatch')

        self.chunk_len=a_chunk_len
        

    # --------------------------------------------------
    # this is the equivalent of toString()
    def __str__(self):
        return "id="+str(self.chunk_id)+" len="+str(self.chunk_len)


# ===================================================================================
# a chunk iterator that can be allocated
class IppChunkIterator():
    
    # -------------------------------------------------------------------------------
    def __init__(self, a_bytearray : bytearray ):
        self._chunks_buf = a_bytearray
        
        self._begin_index=0;
        self._remain_len=len(self._chunks_buf)
        self._error_str=''

    # -------------------------------------------------------------------------------
    # the iterator begins right after the packet len
    def __iter__(self):
        self._begin_index=0;
        self._remain_len=len(self._chunks_buf)
        return self

    # -------------------------------------------------------------------------------
    def __next__(self) -> IppChunk:

        if self._chunks_buf and self._remain_len >= 2:
            return self._getNextIppChunk()
        else:
            raise StopIteration

    # -------------------------------------------------------------------------------
    # there can be more than one chunk in the answer
    # NOTE that now chunks can be of 8 or 16 bits length !
    # TODO, I could dispatch the chunks here !
    def _getNextIppChunk(self) -> IppChunk:
        
        chunk_id=self._chunks_buf[self._begin_index]
        
        if chunk_id & 1 == 0:
            chunk_id,chunk_len=struct.unpack_from('<BB',self._chunks_buf,self._begin_index)
        else:
            chunk_id,chunk_len=struct.unpack_from('<BxH',self._chunks_buf,self._begin_index)
            
        if chunk_len < 2:
            # it happens if you mess up the header on packet creation
            self._error_str="getNextIppChunk: len=0 for id "+str(chunk_id)
            # it is fair to zap the packet, since I would otherwise loop here !
            self._remain_len=0
            return typing.cast(glob_ippqs.IppChunk, None)
        
        chunk_data = self._chunks_buf[self._begin_index:self._begin_index+chunk_len]
        
        if len(chunk_data) != chunk_len:
            # it happens if you mess up the header on packet creation
            self._error_str="getNextIppChunk: BAD len "+str(chunk_len)
            # it is fair to zap the packet, since I would otherwise loop here !
            self._remain_len=0
            return typing.cast(IppChunk, None)
        
        chunk=IppChunk(chunk_data)
        
        self._remain_len  = self._remain_len-chunk_len
        self._begin_index = self._begin_index+chunk_len
        
        return chunk


# ===================================================================================
# Basic class, this is a request
# the difference is that you call it with the chunk id and the length
class IppChunkReq:
    
    # -------------------------------------------------------------------------------
    # NOTE that the rxbuffer is the whole buffer, including the initial chunk bytes
    def __init__(self , chunk_id : int ,chunk_len=0):
        
        if chunk_id == 0:
            raise Exception('chunk_id zero','chunk_id zero')

        self.chunk_id=chunk_id
        
        self.chunk_bytes : bytearray
        
        if chunk_id & 1 == 0:
            self.chunk_bytes = bytearray(struct.pack('<BB',chunk_id,chunk_len))
        else:
            self.chunk_bytes = bytearray(struct.pack('<BxH',chunk_id,chunk_len))

    def get_chunk_len (self) -> int:
        return len(self.chunk_bytes)

    # --------------------------------------------------------------------------
    # append the given data and update the chunk len
    def append_bytearray(self, more_data ):    

        if not more_data:
            return
        
        self.chunk_bytes.extend(more_data)
        
        if self.chunk_id & 1 == 0:
            struct.pack_into('<B',self.chunk_bytes,1,self.get_chunk_len())
        else:
            struct.pack_into('<H',self.chunk_bytes,2,self.get_chunk_len())
        

    # --------------------------------------------------
    # this is the equivalent of toString()
    def __str__(self):
        return "id="+str(self.chunk_id)+" len="+str(self.get_chunk_len())











# ==========================================================================
# an object must implement this class to be called when the apply of a new address is pressed            
class Ipv4_address_apply():
    
    # --------------------------------------------------------------------------
    # you must override this, since python has CRAP abstract  
    def on_ipv4_address_apply(self, ipv4_address : ipaddress.IPv4Address):
        print("on_ipv4_address_apply: NOT overridden")
                
            

# ==========================================================================
class Ipv4_address():

    # ---------------------------------------------------------------------
    # if the given address is invalid, then an empty address
    def __init__(self, a_panel : ttk.Frame ):

        self._ipv4_address = StringVar(a_panel)

        self._apply_callback_list : list[Ipv4_address_apply] = list()

    # -----------------------------------------------------------------------
    # this does NOT call the list of listeners !!!
    def set_value_string(self, new_value : str ):
        self._ipv4_address.set(new_value)
        
    # -----------------------------------------------------------------------
    def get_value_string(self) -> str:
        return self._ipv4_address.get()
        
    # -----------------------------------------------------------------------
    # it may return None if the conversion is invalid
    def get_value_ipv4(self) -> ipaddress.IPv4Address:
        try:
            return ipaddress.IPv4Address(self.get_value_string())
        except Exception as _exc :
            return typing.cast(ipaddress.IPv4Address,None)
        
    # -----------------------------------------------------------------------
    # add a method to call to the apply callback list
    def add_to_apply_callback_list(self, an_objectt : Ipv4_address_apply ):
        
        if an_objectt:
            self._apply_callback_list.append(an_objectt)
        
    # -----------------------------------------------------------------------
    # you can call this whenever you wish to signal ip address changed
    def on_apply_button_pressed(self):
        
        ipv4_address = self.get_value_ipv4()
        
        for a_listener in self._apply_callback_list:
            try:
                a_listener.on_ipv4_address_apply(ipv4_address)
            except Exception as _exc:
                pass        

            
# ==========================================================================
# I need to wrap the idiotic socket
# This is for TCP ONLY, Udp will be handled by another class
# this is a kind of duplicated of QswioStreamSocket, try to merge them
class WisocketTcp():            

    # -----------------------------------------------------------------------
    # simpler form
    def __init__(self):
        
        self._socket : socket.socket = typing.cast(socket.socket,None)
        
    # ------------------------------------------------------
    # just close the damm socket
    def socket_close(self):

        if self._socket is None:
            return 

        try:
            self._socket.close()
        except Exception as _err:
            pass
        
    # ------------------------------------------------------
    # this creates a new socket since python is idiotic and cannot reuse a descriptor
    def socket_connect(self, address_str ):

        self.socket_close()
        
        self._socket = socket.create_connection(address_str)
        
        
    # ------------------------------------------------------
    def socket_sendall(self, a_bytes ):
        
        self._socket.sendall(a_bytes)
        
    # ------------------------------------------------------
    # need to do this one
    def socket_rcvall(self, want_len ):

        risul = bytearray()
                
        while want_len > 0:
            received_bytes = self._socket.recv(want_len)
        
            risul.extend(received_bytes)
            
            have_len = len(received_bytes)
            
            if have_len == want_len:
                return risul
        
            # what I want is less
            want_len = want_len - have_len
            
        
        
        
        
        