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


 NOTE remember that next() of an iterator HAS a default value parameter is the next is NOT found !

'''

from __future__ import annotations

from datetime import datetime, timezone
import pathlib
import platform
import pyperclip  # type: ignore
import re
import socket
import subprocess
import sys
import time
import tkinter
import webbrowser


# -------------------------------------------------------------------
# It is called 1x since one digit is missing in the value to arrive to the Hz
def frequency_1x_to_Mhz_str(freq_1x) -> str:
    return "%d.%05d" % (int(freq_1x/100000), int(freq_1x % 100000))

# -------------------------------------------------------------------
# It is called 1x since one digit is missing in the value to arrive to the Hz
def frequency_1x_to_kHz_str(freq_1x) -> str:
    return "%d.%02d" % (int(freq_1x/100), int(freq_1x % 100))

# ------------------------------------------------------------------
# the incoming string is the decimal part of a Mhz frequency, missing one "decimal" (five digits)
# so, max val is 99999 and min val is 00000 and this is the range that must be returned
# the input CAN be shorter than five digits and it is assumed padded right with 0 to five digits
# if there are zeros on the left, the result .... is fine, .... no ?
# So, the difficult part is to "pad" to arrive to five digits
def frequency_mHz_dec_to_int_1x(v_in : str ) -> int:
    
    if not v_in:
        return 0
    
    s_len=len(v_in)
    o_val = int(v_in)
    
    while s_len < 5:
        o_val = o_val * 10
        s_len += 1
    
    return o_val


# ------------------------------------------------------------------
# like the previous one byt for kHz
def frequency_kHz_dec_to_int_1x(v_in : str ) -> int:
    
    if not v_in:
        return 0
    
    s_len=len(v_in)
    o_val = int(v_in)
    
    while s_len < 2:
        o_val = o_val * 10
        s_len += 1
    
    return o_val



# -------------------------------------------------------------------
# Convert a string to a boollean and if no match return the given default
# apparently, this is the correct way to map a boolean to a boolean...
def string_to_bool(a_str : str, a_default : bool ) -> bool:
    
    if not a_str:
        return a_default
    
    if a_str in ['True','Yes','1']:
        return True
    else:
        return False
    
# -------------------------------------------------------------------
# sometime I wish for a different visual from booleans
def bool_to_string(a_bool : bool, v_false : str, v_true : str ) -> str:
    if a_bool:
        return v_true
    else:
        return v_false
        
        
# -------------------------------------------------------------------
# since this CRAP LANGUAGE cannot properly handle types, ther eis a LOT of going back and forth from
# string to int AND the IDIOTS did not thing to have a rule that an invalid conversion result in zero value
# If you are making a shit language, you may as well do it properly
# so.... one need to write this stupid fun
# NOTE remember that next() of an iterator HAS a default value parameter is the next is NOT found !        
def string_to_int(a_string : str , def_value : int ) -> int:
    try:
        return int(a_string)
    except Exception as _exc:
        return def_value        
    
# -------------------------------------------------------------------
# since this CRAP LANGUAGE cannot properly handle types, ther eis a LOT of going back and forth from
def string_to_float(a_string : str , def_value : float ) -> float:
    try:
        return float(a_string)
    except Exception as _exc:
        return def_value        

# ----------------------------------------------------------------------
# convert what could be a hex to an int
# if the parameterbegins with 0x or it has a A,B,C,D,E,F letter then it is a hex
def string_to_int_hex ( a_string : str, def_value : int ) -> int:
    try:
        if a_string[:2] == '0x':
            return int(a_string[2:],16)
        elif re.search('[a-fA-F]', a_string):
            return int(a_string,16)
        else:
            return int(a_string,10)
    except Exception as _exc:
        return def_value
    
    


# -------------------------------------------------------------------
# NOTE that it returns a Path !
# this has the logic to work with pyinstaller https://pyinstaller.org/en/stable/
# the idea is tha if the attribute is defined then the resources are in there
# otherwise they are in the resource directory, relative to this current file
# NOTE that slash is overloaded to mean join and make a subpath ...
def getApplicationResourcesPath() -> pathlib.Path:
    if hasattr(sys, '_MEIPASS'):
        return pathlib.Path(sys._MEIPASS) / 'resources'
    else:
        return pathlib.Path(__file__).parent / 'resources'

# ------------------------------------------------------------------
# when you want the full path fiven the fname
def getResourcesFname(f_name : str ) -> pathlib.Path:
    return getApplicationResourcesPath() / f_name


# -------------------------------------------------------------------
# return a PhotoImage that pick up the file from where this module is
# You have to put the png files where this module is
# If for some reason the image is not found then an empty image is returned
# you can subsample the photoimage !
def getResourcesPhotoimage(filename) -> tkinter.PhotoImage:
    try:
        fpath : pathlib.Path = getApplicationResourcesPath() / filename
        return tkinter.PhotoImage(file = fpath)
    except Exception as _exc:
        print("getResourcesPhotoimage: ",_exc," cannot find ",filename)
        return tkinter.PhotoImage(file="") 
        

# ------------------------------------------------------------------
# Should return the full path, relative to user home directory
def getHomeFileName(filename) -> pathlib.Path:
    return pathlib.Path.home() / filename

# ----------------------------------------------------------------------
# the idea is that this file holds the shared part of the system
# especially relevant is the qpystat that hold the state seen by the client and the server
def get_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.settimeout(0)
    try:
        # doesn't even have to be reachable
        s.connect(('10.255.255.255', 1))
        IP = s.getsockname()[0]
    except Exception:
        IP = '127.0.0.1'
    finally:
        s.close()
    return IP


# ---------------------------------------------------------------------
# To make it type safe and clear
# return the current time as seconds from the epoch
def getCurrentTime_s() -> int :
    afloat = time.time()
    return int(afloat)

# ----------------------------------------------------
# Attempt to retrieve the given key from the dictionary
# return the default value if not found, no exception
def getDictStringValue(adict : dict, key : str , defvalue : str ) -> str:
    try:
        return str(adict[key])
    except Exception as _exc:
        return defvalue
    
# ----------------------------------------------------
# get a value as an int
def getDictIntValue(adict : dict, key , defvalue : int ) -> int:
    try:
        return int(adict[key])
    except Exception as _exc:
        return defvalue



# --------------------------------------------------------------------
# get the current time a number of seconds since the epoch
# This is mostly here to remember the way python does it...
# NOTE: The time returned is the time NOW that is in UTC, tested equivalent to
# dt = datetime.now(timezone.utc)
# dt.timestamp()
def get_utc_timestamp_s () -> int:
    afloat = time.time()
    return int(afloat)

# ---------------------------------------------------------------------
# return the UTC as milliseconds
def get_utc_timestamp_milli() -> int:
    return round(time.time() * 1000)

# ---------------------------------------------------------------------
# This returns a datetime
# It is basically, whatever is display in the computer, as it is
# it does not have timezone information and therefore... it is a mess
# https://docs.python.org/3/library/datetime.html
def getDatetimeNow() -> datetime:
    return datetime.now()

# ---------------------------------------------------------------------
# This returns a datetime that is rooted in UTC
# https://docs.python.org/3/library/datetime.html
def getUtcDatetimeNow() -> datetime:
    return datetime.now(timezone.utc)

# ---------------------------------------------------------------------
# Convert from unix timestamp (number of seconds since epoch) to a ISO string
# The incoming timestamp is supposed to be in UTC timezone, this is normally the case if coming from a sqllite3
def fromUtcTimestampToIsoString(time_s : int) -> str:
    dt = datetime.fromtimestamp(time_s,timezone.utc)
    return dt.strftime('%Y-%m-%d %H:%M:%S')

# -----------------------------------------------------------------------
# Convert from a UTC string to the unix time (seconds) assuming we are in UTC timezone
# PYTHON time handling is a mess in all languages.... especially python
def fromUtcIsoStringToTimestamp ( datetime_str : str ) -> int:
    dt = datetime.fromisoformat(datetime_str)
    dt = dt.replace(tzinfo=timezone.utc)
    af = dt.timestamp()
    return int(af)

# ---------------------------------------------------------------------
# Convert from unix timestamp (number of seconds since epoch) to a ISO string
def fromTimestampToIsoString(time_s : int) -> str:
    dt = datetime.fromtimestamp(time_s)
    return dt.strftime('%Y-%m-%d %H:%M:%S')

# -----------------------------------------------------------------------
# Convert from a string to the unix time (seconds) 
# PYTHON time handling is a mess in all languages.... especially python
def fromIsoStringToTimestamp ( datetime_str : str ) -> int:
    dt = datetime.fromisoformat(datetime_str)
    af = dt.timestamp()
    return int(af)


    
# ----------------------------------------------------------------------
# I have a unix timestamp and I basically want to strip the hours, minutes, seconds
# This sequence of operation is correct, do not touch the timezone issue at all and it works
def fromTimestampToDateTimestamp(datetime_s : int) -> int:    
    dt = datetime.fromtimestamp(datetime_s)
    dt.replace(hour=0, minute=0, second=0)
    af = dt.timestamp()
    return int(af)


# -----------------------------------------------------------------
# @return 0 -> 15 if valid, 16 if invalid
def fromHexDigitToByte ( digit ):
    if digit == '0':
        return 0;
    elif digit == '1':
        return 1;
    elif digit == '2':
        return 2;
    elif digit == '3':
        return 3;
    elif digit == '4':
        return 4;
    elif digit == '5':
        return 5;
    elif digit == '6':
        return 6;
    elif digit == '7':
        return 7;
    elif digit == '8':
        return 8;
    elif digit == '9':
        return 9;
    elif digit == 'A' or digit == 'a':
        return 10;
    elif digit == 'B' or digit == 'b':
        return 11;
    elif digit == 'C' or digit == 'c':
        return 12;
    elif digit == 'D' or digit == 'd':
        return 13;
    elif digit == 'E' or digit == 'e':
        return 14;
    elif digit == 'F' or digit == 'f':
        return 15;
    else:
        return 16;


# -----------------------------------------------------------------
# @return 0 -> 15 if valid, 16 if invalid
def fromDtmfDigitToByte ( digit ):
    if digit == '0':
        return 0;
    elif digit == '1':
        return 1;
    elif digit == '2':
        return 2;
    elif digit == '3':
        return 3;
    elif digit == '4':
        return 4;
    elif digit == '5':
        return 5;
    elif digit == '6':
        return 6;
    elif digit == '7':
        return 7;
    elif digit == '8':
        return 8;
    elif digit == '9':
        return 9;
    elif digit == 'A' or digit == 'a':
        return 10;
    elif digit == 'B' or digit == 'b':
        return 11;
    elif digit == 'C' or digit == 'c':
        return 12;
    elif digit == 'D' or digit == 'd':
        return 13;
    elif digit == '*':
        return 14;
    elif digit == '#':
        return 15;
    else:
        return 16;






# -------------------------------------------------------------------------
# utility
def bits_setBooleanBit ( v_in : int, bit_shift : int, b_val : bool ) -> int:
    mask = 1 << bit_shift
    
    if b_val:
        return v_in | mask
    else:
        return v_in & ~mask;
    
# -------------------------------------------------------------------------
# @bit_mask the mask to use for the value BEFORE the shift
def bits_setBitsValue(v_in : int, bit_shift, bit_mask : int, b_val ) -> int:
    
    b_val &= bit_mask
    
    shifted_mask = bit_mask << bit_shift
    
    v_clean = v_in & ~shifted_mask
    
    return v_clean | (b_val << bit_shift)
    

# -------------------------------------------------------------------------
def bits_getBooleanBit(v_in : int , bit_shift : int ) -> bool:
    
    mask = 1 << bit_shift
    
    return v_in & mask != 0

# -------------------------------------------------------------------------
# extract from the given int the given (contigous) bit mask shifted of the given shift
def bits_getBitsValue(v_in : int , bit_shift : int, bit_mask : int ) -> int:
    
    v_val = v_in >> bit_shift
    
    return v_val & bit_mask

# -------------------------------------------------------------------------
# attempt a clipboard copy
# @return true if it manage it
def clipboard_copy(a_string : str) -> bool:
    # this will trigger the binding
    pyperclip.copy(a_string)
    # and this will report if the binding is OK
    return pyperclip.is_available()

def clipboard_paste() -> str:
    if pyperclip.is_available():
        return pyperclip.paste()
    else:
        return ''
    
    
# ------------------------------------------------------------------------------
# idiotic apple mac has a different way to do a simple thing
# NOT better, just different    
def open_pdf_file (  filepath : pathlib.Path ):
    
    print("open_pdf_file ",filepath)
    
    if platform.system() == 'Darwin':       # macOS
        subprocess.call(('open', filepath))
    else:
        webbrowser.open_new(str(filepath)) 




