import os
import math
import RNS
import RNS.vendor.umsgpack as mp

def simulate(link_speed=9600, audio_slot_ms=70, codec_rate=1200, method="msgpack"):
    # Simulated on-air link speed
    LINK_SPEED = link_speed

    # Packing method, can be "msgpack" or "protobuf"
    PACKING_METHOD = method

    # The target audio slot time
    TARGET_MS = audio_slot_ms

    # Packets needed per second for half-duplex audio
    PACKETS_PER_SECOND = 1000/TARGET_MS

    # Effective audio encoder bitrate
    CODEC_RATE = codec_rate

    # Maximum number of supported audio modes
    MAX_ENUM = 127

    # Per-packet overhead on a established link is 19
    # bytes, 3 for header and context, 16 for link ID
    RNS_OVERHEAD = 19

    # Physical-layer overhead. For RNode, this is 1
    # byte per RNS packet.
    PHY_OVERHEAD = 1

    # Total transport overhead
    TRANSPORT_OVERHEAD = PHY_OVERHEAD+RNS_OVERHEAD

    # Calculate parameters
    AUDIO_LEN = int(math.ceil(CODEC_RATE/(1000/TARGET_MS)/8))
    PER_BYTE_LATENCY_MS = 1000/(LINK_SPEED/8)

    # Pack the message with msgpack to get real-
    # world packed message size

    if PACKING_METHOD == "msgpack":
        # Calculate msgpack overhead
        PL_LEN = len(mp.packb([MAX_ENUM, os.urandom(AUDIO_LEN)]))
        PACKING_OVERHEAD = PL_LEN-AUDIO_LEN
    elif PACKING_METHOD == "protobuf":
        # For protobuf, assume the 8 bytes of stated overhead
        PACKING_OVERHEAD = 8
        PL_LEN = AUDIO_LEN+PACKING_OVERHEAD
    else:
        print("Unsupported packing method")
        exit(1)

    # Calculate required encrypted token blocks
    BLOCKSIZE = 16
    REQUIRED_BLOCKS = math.ceil((PL_LEN+1)/BLOCKSIZE)
    ENCRYPTED_PAYLOAD_LEN = REQUIRED_BLOCKS*BLOCKSIZE
    BLOCK_HEADROOM = (REQUIRED_BLOCKS*BLOCKSIZE) - PL_LEN - 1

    # The complete on-air packet length
    PACKET_LEN     = PHY_OVERHEAD+RNS_OVERHEAD+ENCRYPTED_PAYLOAD_LEN
    PACKET_LATENCY = round(PACKET_LEN*PER_BYTE_LATENCY_MS, 1)

    # TODO: This should include any additional
    # airtime consumption such as preamble and TX-tail.
    PACKET_AIRTIME = PACKET_LEN*PER_BYTE_LATENCY_MS
    AIRTIME_PCT = (PACKET_AIRTIME/TARGET_MS) * 100

    # Maximum amount of concurrent full-duplex
    # calls that can coexist on the same channel
    CONCURRENT_CALLS = math.floor(100/AIRTIME_PCT)

    # Calculate latencies
    TRANSPORT_LATENCY  = round((PHY_OVERHEAD+RNS_OVERHEAD)*PER_BYTE_LATENCY_MS, 1)

    PAYLOAD_LATENCY    = round(ENCRYPTED_PAYLOAD_LEN*PER_BYTE_LATENCY_MS, 1)
    RAW_DATA_LATENCY   = round(AUDIO_LEN*PER_BYTE_LATENCY_MS, 1)
    PACKING_LATENCY    = round(PACKING_OVERHEAD*PER_BYTE_LATENCY_MS, 1)
    
    DATA_LATENCY       = round(ENCRYPTED_PAYLOAD_LEN*PER_BYTE_LATENCY_MS, 1)
    ENCRYPTION_LATENCY = round((ENCRYPTED_PAYLOAD_LEN-PL_LEN)*PER_BYTE_LATENCY_MS, 1)
    if ENCRYPTED_PAYLOAD_LEN-PL_LEN == 1:
        E_OPT_STR = "(optimal)"
    else:
        E_OPT_STR = "(sub-optimal)"

    TOTAL_LATENCY      = round(TARGET_MS+PACKET_LATENCY, 1)

    print( "\n===== Simulation Parameters ===\n")
    print(f"  Packing method       : {method}")
    print(f"  Sampling delay       : {TARGET_MS}ms")
    print(f"  Codec bitrate        : {CODEC_RATE} bps")
    print(f"  Audio data           : {AUDIO_LEN} bytes")
    print(f"  Packing overhead     : {PACKING_OVERHEAD} bytes")
    print(f"  Payload length       : {PL_LEN} bytes")
    print(f"  AES blocks needed    : {REQUIRED_BLOCKS}")
    print(f"  Encrypted payload    : {ENCRYPTED_PAYLOAD_LEN} bytes")
    print(f"  Transport overhead   : {TRANSPORT_OVERHEAD} bytes ({RNS_OVERHEAD} from RNS, {PHY_OVERHEAD} from PHY)")
    print(f"  On-air length        : {PACKET_LEN} bytes")
    print(f"  Packet airtime       : {PACKET_AIRTIME}ms")

    print( "\n===== Results for "+RNS.prettyspeed(LINK_SPEED)+" Link Speed ===\n")
    print(f"  Final latency        : {TOTAL_LATENCY}ms")
    print(f"    Recording latency  : contributes {TARGET_MS}ms")
    print(f"    Packet transport   : contributes {PACKET_LATENCY}ms")
    print(f"      Payload          : contributes {PAYLOAD_LATENCY}ms")
    print(f"        Audio data     : contributes {RAW_DATA_LATENCY}ms")
    print(f"        Packing format : contributes {PACKING_LATENCY}ms")
    print(f"        Encryption     : contributes {ENCRYPTION_LATENCY}ms {E_OPT_STR}")
    print(f"      RNS+PHY overhead : contributes {TRANSPORT_LATENCY}ms")
    print(f"")
    print(f"  Half-duplex airtime  : {round(AIRTIME_PCT, 2)}% of link capacity")
    print(f"    Concurrent calls   : {int(CONCURRENT_CALLS)}\n")
    print(f"  Full-duplex airtime  : {round(AIRTIME_PCT*2, 2)}% of link capacity")
    print(f"    Concurrent calls   : {int(CONCURRENT_CALLS/2)}")

    if BLOCK_HEADROOM != 0:
        print("")
        print(f"  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
        print(f"  Unaligned AES block! Each packet could fit")
        print(f"  {BLOCK_HEADROOM} bytes of additional audio data")
        print(f"  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")

print(  "\n= With mspack =================")
simulate(method="msgpack")

#print("\n\n= With protobuf ===============")
#simulate(method="protobuf")