# Transmit file to the GALAXY computer
#
# Dejan Ristanovic 2024
#
# v1.0: 21.03.2024
# v1.1: 22.03.2024
# v2.0: 30.03.2024.
# v2.1: 03.04.2024.
# v2.2: 05.04.2024. case sensitive function for Unix by Vlado Vince
# v2.5: 14.04.2024.

# needs: pip install pyserial

import serial
import time
import os
import sys

version = "2.50"
checksums = 0
checksumr = 0
debug = True

def mydir():
    if getattr(sys, 'frozen', False):
        # Running as a bundled executable
        return os.path.dirname(sys.executable)
    else:
        # Running as a script
        return os.path.dirname(os.path.abspath(__file__))

def debug_print_hex_dump(title, data):
    hex_str = data.hex()
    ascii_str = ''.join(chr(byte) if 32 <= byte <= 126 else '.' for byte in data)
    print(title)
    for i in range(0, len(hex_str), 32):
        address = i // 2
        hex_line = hex_str[i:i+32]
        hex_formatted = ' '.join(hex_line[j:j+2] for j in range(0, len(hex_line), 2))
        ascii_line = ascii_str[i//2:i//2+16]  # Adjusting ASCII representation
        print(f"{address:04X}  {hex_formatted: <47} |{ascii_line}|")

def send_data(data, ser):
    global debug
    global checksums
#   time.sleep(0.1) # 100 ms
    for byte in data: checksums += byte
    try:
       ser.write(data)
    except serial.SerialException as e:
       print("Serial port error:", e)
       sys.exit()
    except serial.SerialTimeoutException:
       print("Timeout occurred during serial communication.")
       sys.exit()
    except KeyboardInterrupt:
       print("Keyboard interrupt detected. Exiting...")
       sys.exit()
    if debug: debug_print_hex_dump("Sending data:", data)

def galfname(string):
    string = string.upper()
    string = string.replace(" ", "")
    if string.endswith(".GTP"):
       string = string[:-4]
    allowed_characters = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.")
    string = ''.join(char for char in string if char in allowed_characters)
    if len(string) > 14:
       string = string[:14]
    return string

def get_byte (ser, debg=False):
   global checksumr
   try:
      data = ser.read()
      if data:
         decimal_value = int.from_bytes(data, byteorder='big')  # Convert byte to decimal
         if debg:
            decimal_formatted = '{:03d}'.format(decimal_value)
            hex_value = data.hex()  # Convert byte to hexadecimal
            ascii_char = data.decode('ascii', errors='replace') if decimal_value>=32 and decimal_value<127 else '.'
            print("Byte received: ", decimal_formatted, hex_value, ascii_char)
         checksumr=checksumr+decimal_value
         return data
      else:
         print("Timeout reached. No data received for {} seconds. Exiting.".format(timeout))
         sys.exit()
   except KeyboardInterrupt:
         print("Keyboard interrupt. Exiting.")
         sys.exit()

def get_block(ser, length):
    global debug
    file_data = b''
    for ii in range(length):
        byte = get_byte(ser, False)
        if byte is None:
            break
        file_data += byte
    if debug: debug_print_hex_dump("Received data: ", file_data)
    return file_data

def open_serial_port(port, baudrate=19200, parity='N', timeout=10):
    try:
        ser = serial.Serial(port, baudrate, parity=parity, timeout=timeout)
    except serial.serialutil.SerialException as e:
        print("Serial communication error:", e)
    except PermissionError as e:
        print("Permission error:", e)
    except ValueError as e:
        print("Invalid parameter:", e)
    except Exception as e:
        print("Unexpected error:", e)
    else:
        return ser
    sys.exit()

def getfile_insensitive(path): # Get file name with correct capitalization
    directory, filename = os.path.split(path)
    directory, filename = (directory or '.'), filename.lower()
    for f in os.listdir(directory):
        newpath = os.path.join(directory, f)
        if os.path.isfile(newpath) and f.lower() == filename:
            return newpath

def isfile_insensitive(path): # Check if file name exists with different capitalization
    return getfile_insensitive(path) is not None

def transmit_file(port, baudrate=19200, parity='N', timeout=10, filename="", multiple=False):
    global checksums
    global checksumr
    global debug
    if multiple:
       print ("To receive next program, type OLD # on the GALAXY computer")
    ser = open_serial_port(port, baudrate, parity, timeout)
    checksums=0
    checksumr=0
    filename=filename.upper()
    if filename.endswith(".GTP"):
       filename = filename[:-4]
    waiting=True
    while waiting:
          data=get_byte (ser, debug)
          if ord(data)!=0xa3: continue
          waiting=False
    fn=""
    block=get_block(ser, 14)
    for byte in block:
        if byte>32 and byte<127: fn+=chr(byte)
    if fn.endswith("GTP"): fn=fn[:-4]
    print("Asking for file: "+fn)
    if filename=="" and fn=="":
       print ("Filename not provided")
       data_to_send = bytes([0xCC, 0xDD, 0xEE, 0xF4])
       send_data(data_to_send, ser)
       sys.exit()
    elif fn=="":
          fn=filename
    elif filename=="":
          pass
    elif filename.upper()!=fn.upper():
          print ("Conflicting filenames")
          data_to_send = bytes([0xCC, 0xDD, 0xEE, 0xF1])
          send_data(data_to_send, ser)
          return
    file_path = os.path.join(mydir(), fn)
    if not os.path.isfile(file_path):
       file_path=file_path+".gtp"
       if isfile_insensitive(file_path): # Check if file name exists with different capitalization
          insensitive_path=getfile_insensitive(file_path) # Get file name with correct capitalization
          file_path=insensitive_path # Updates file_path with correctly capitalized file name
       if not os.path.isfile(file_path): 
          print ("No file: "+file_path)
          data_to_send = bytes([0xCC, 0xDD, 0xEE, 0xF2])
          send_data(data_to_send, ser)
          sys.exit()
       print ("Transfering: "+file_path)
       data_to_send = bytes([0xCC, 0xDD, 0xEE, 0xF0])
       send_data(data_to_send, ser)
       trans_name=galfname(fn)
       trans_name_array = trans_name.encode('ascii')
       trans_name_array += b'\x00' * (14 - len(trans_name_array))
       with open(file_path, 'rb') as f:
            file_data = f.read()
            if file_data[0]==0x10: # extended GTP format
               name_length=file_data[1]
               file_data=file_data[name_length+5:]
            if file_data[5]==0xa5:
               data_to_send = bytes([0x00,0x00, 0x00, 0x00, 0x00, 0xa5])
               send_data(data_to_send, ser)
               word1 = file_data[6] + (file_data[7] << 8)
               word2 = file_data[8] + (file_data[9] << 8)
               word3 = file_data[10] + (file_data[11] << 8)
               word4 = file_data[12] + (file_data[13] << 8)
               actual_file_length=len(file_data)
               calculated_file_length=word2 - word1 + 11
               checksums=0xa5
               send_data(trans_name_array, ser)
               if calculated_file_length==actual_file_length:
                  send_data(file_data[6:-1], ser)
               else:
                  send_data(file_data[6:-2], ser)
               send_data(bytes([0x20]), ser)
               checksums=checksums & 0xff
               checksum_byte = checksums.to_bytes(1, byteorder='little')  # Convert checksum to a single byte
               send_data(checksum_byte, ser)
            else:
               print ("Wrong file format")
               data_to_send = bytes([0xCC, 0xDD, 0xEE, 0xF5])
               send_data(data_to_send, ser)
               return
            f.close()
            data_to_send = bytes([0xCC, 0xDD, 0xEE, 0xF0]) # OK report
            send_data(data_to_send, ser)
            print ("To save this program in the EEPROM, on the GALAXY computer type:")
            if word1 != 0x2c36 or word4 != word2:
               w1hex=hex(word1)[2:].upper()
               w2hex=hex(word2-1)[2:].upper()
               print ("SAVE "+trans_name+", &"+w1hex+", &"+w2hex)
            else:
               print ("SAVE "+trans_name)
    ser.close()

def read_cfg_port():
    current_directory = mydir()
    file_path = os.path.join(current_directory, "comport.txt")
    if os.path.exists(file_path):
        with open(file_path, 'r') as file:
            port = file.readline().strip()
        return port
    else:
        return None

def printusage():
    print("Usage: python transmit.py [<file_name>] [<COM_port>]")
    print("   or: python transmit.py * [<COM_port>]")

if __name__ == "__main__":
    print ("GALAXY Transmit Utility v", version)
    print ("Copyright (C) 2024 by Dejan Ristanovic")
    timeout = 60  # Timeout set to 60 seconds
    debug=False # Run in debug mode, print communication on screen
    port = read_cfg_port()
    if not port:
       port = "com4"
    if len(sys.argv) == 1:
        file_name = ''
    elif len(sys.argv) == 2:
        file_name = sys.argv[1]
        if file_name == "?" or file_name=="/?":
           printusage()
           sys.exit(1)
    elif len(sys.argv) == 3:
        file_name = sys.argv[1]
        port = sys.argv[2]
    else:
        printusage()
        sys.exit(1)
    if file_name=="*":
       nntransfered=0
       dir=mydir()
       files = os.listdir(dir)
       gtp_files = [file for file in files if file.lower().endswith('.gtp')]
       if gtp_files:
          for file in gtp_files:
              transmit_file(port, timeout=timeout, filename=file, multiple=True)
              nntransfered+=1
          print ("Transfered", nntransfered, "files.")
       else:
          print("No .gtp files found in: ", dir)
    else:
       transmit_file(port, timeout=timeout, filename=file_name, multiple=False)
