import struct
import numpy as np
import time
from pylora import LoRa
from scipy.signal import resample

# -----------------------------
# Configuration
# -----------------------------
STRANDS = 8
SLOTS = 4
PACKET_SIZE = STRANDS*SLOTS + 1
SAMPLE_RATE = 2.048e6
NODE_COUNT = 4
ALPHA = 0.3  # Smith-graph resonance
RIDE_FACTOR = 0.5  # How strongly the lattice rides the external signal

# -----------------------------
# Initialize LoRa
# -----------------------------
lora = LoRa()
lora.set_mode(LoRa.RX)
lora.set_frequency(868e6)
lora.set_sf(7)
lora.set_bw(125e3)
lora.set_cr(5)

# -----------------------------
# Node lattice storage
# -----------------------------
nodes = {i: np.zeros((STRANDS, SLOTS)) for i in range(NODE_COUNT)}
ticks = {i: 0 for i in range(NODE_COUNT)}

# -----------------------------
# Decode packet
# -----------------------------
def decode_packet(packet):
    flat = list(struct.unpack("B"*STRANDS*SLOTS + "B", packet))
    tick = flat[-1]
    lattice = np.array(flat[:-1]).reshape((STRANDS, SLOTS)) / 50.0
    return lattice, tick

# -----------------------------
# Smith-graph cross-node resonance
# -----------------------------
def smith_resonance(nodes):
    node_list = list(nodes.values())
    blended_nodes = {}
    for i, lattice in enumerate(node_list):
        resonance = np.zeros_like(lattice)
        for j, other in enumerate(node_list):
            if i == j:
                continue
            resonance += other
        resonance /= (len(node_list)-1)
        blended_nodes[i] = (1-ALPHA)*lattice + ALPHA*resonance
    return blended_nodes

# -----------------------------
# Aggregate lattices
# -----------------------------
def aggregate_lattice(nodes):
    lattices = np.array(list(nodes.values()))
    agg = np.mean(lattices, axis=0)
    return agg

# -----------------------------
# Convert lattice to IQ
# -----------------------------
def lattice_to_iq(lattice, length=1024, carrier=None):
    t = np.arange(length) / SAMPLE_RATE
    signal = np.zeros(length)

    # Lattice modulation
    for s in range(STRANDS):
        weight = np.mean(lattice[s])
        freq = 1e3*(s+1)
        signal += weight * np.sin(2*np.pi*freq*t)

    # Ride external carrier
    if carrier is not None:
        if len(carrier) != length:
            carrier = resample(carrier, length)
        signal = (1-RIDE_FACTOR)*carrier + RIDE_FACTOR*signal

    # Normalize
    signal /= np.max(np.abs(signal))
    return signal.astype(np.complex64)

# -----------------------------
# Simulated environmental carrier (replace with SDR input)
# -----------------------------
def get_environmental_signal(length=2048):
    # Example: mix of sine waves representing ambient signal
    t = np.arange(length) / SAMPLE_RATE
    env = 0.3*np.sin(2*np.pi*50e3*t) + 0.2*np.sin(2*np.pi*75e3*t)
    return env

# -----------------------------
# Main loop
# -----------------------------
print("[+] HDGL Smith-graph aggregator with environmental riding running...")
while True:
    if lora.packet_available():
        packet = lora.receive_packet()
        node_id = packet[0] % NODE_COUNT
        lattice, tick = decode_packet(packet)
        nodes[node_id] = lattice
        ticks[node_id] = tick

    # Apply Smith-graph resonance
    nodes = smith_resonance(nodes)

    # Aggregate lattice
    agg_lattice = aggregate_lattice(nodes)

    # Fetch environmental signal
    env_signal = get_environmental_signal(length=2048)

    # Convert to IQ riding the environment
    iq_signal = lattice_to_iq(agg_lattice, length=2048, carrier=env_signal)

    # IQ ready for TX or visualization
    binary_agg = (agg_lattice > 1.0).astype(int).flatten()
    hex_agg = int(binary_agg.dot(1 << np.arange(STRANDS*SLOTS)))
    print(f"Aggregated Lattice Hex: 0x{hex_agg:08x}")

    time.sleep(0.1)
