import raylibpy as rl
import math as m
import socket
import threading
import queue
import json
import base64
import random
import hashlib
from cryptography.fernet import Fernet

class EventManager:
    def __init__(self):
        self._listeners = {}
    def On(self, event_name, callback):
        if event_name not in self._listeners:
            self._listeners[event_name] = []
        self._listeners[event_name].append(callback)
    def Off(self, event_name, callback):
        if event_name in self._listeners:
            try:
                self._listeners[event_name].remove(callback)
            except ValueError:
                pass
    def Emit(self, event_name, *args, **kwargs):
        if event_name in self._listeners:
            for callback in self._listeners[event_name]:
                callback(*args, **kwargs)

def DrawRect(x, y, width, height, colour):
    rl.draw_rectangle(x, y, width, height,
                      rl.Color(colour[0], colour[1], colour[2], colour[3]))
def DrawRectRounded(x, y, width, height, roundness, sections, colour):
    rrect = rl.Rectangle(x, y, width, height)
    rl.draw_rectangle_rounded(rrect, roundness, sections,
                              rl.Color(colour[0], colour[1], colour[2], colour[3]))
def DrawCircle(x, y, radius, colour):
    rl.draw_circle(x, y, radius,
                   rl.Color(colour[0], colour[1], colour[2], colour[3]))

class Sprite():
    def __init__(self, camera_follow: bool = False, x: float = 0.0, y: float = 0.0,
                 rot: float = 0.0, scale: float = 1.0, controls: dict = {},
                 textures: list = [], data: dict = {}):
        self.camera_follow = camera_follow
        self.x = x
        self.y = y
        self.rot = rot
        self.scale = scale
        self.controls = controls
        self.textures = textures
        self.data = data
        self.body = None
    def Tick(self):
        for func, key in self.controls.items():
            if rl.is_key_down(key):
                func()
    def Draw(self, x=None, y=None, rot=None, scale=None, texture_id=0, tint=None):
        if x is None: x = self.x
        if y is None: y = self.y
        if rot is None: rot = self.rot
        if scale is None: scale = self.scale
        if tint is None: tint = [255, 255, 255, 255]
        if 0 <= texture_id < len(self.textures):
            rl.draw_texture_ex(self.textures[texture_id],
                               rl.Vector2(x, y), rot, scale,
                               rl.Color(tint[0], tint[1], tint[2], tint[3]))

class Vector2:
    def __init__(self, x=0.0, y=0.0):
        self.x = x
        self.y = y
    def __add__(self, other):
        return Vector2(self.x + other.x, self.y + other.y)
    def __sub__(self, other):
        return Vector2(self.x - other.x, self.y - other.y)
    def __mul__(self, scalar):
        return Vector2(self.x * scalar, self.y * scalar)
    def __truediv__(self, scalar):
        return Vector2(self.x / scalar, self.y / scalar)
    def Length(self):
        return m.sqrt(self.x * self.x + self.y * self.y)
    def Normalize(self):
        l = self.Length()
        if l != 0:
            self.x /= l
            self.y /= l
        return self
    def Copy(self):
        return Vector2(self.x, self.y)

class RigidBody:
    def __init__(self, mass=1.0, gravity_scale=1.0, linear_damping=0.0):
        self.mass = mass
        self.gravity_scale = gravity_scale
        self.linear_damping = linear_damping
        self.velocity = Vector2()
        self.acceleration = Vector2()
        self.force = Vector2()
        self.is_static = False
    def ApplyForce(self, force):
        self.force += force
    def Integrate(self, dt, gravity):
        if self.is_static:
            return
        net_force = self.force + (gravity * self.gravity_scale * self.mass)
        self.acceleration = net_force / self.mass
        self.velocity += self.acceleration * dt
        self.velocity *= (1.0 - self.linear_damping)
        self.force = Vector2()

class CollisionShape:
    pass
class CircleShape(CollisionShape):
    def __init__(self, radius):
        self.radius = radius
class AABBShape(CollisionShape):
    def __init__(self, half_width, half_height):
        self.half_width = half_width
        self.half_height = half_height

class PhysicsWorld:
    def __init__(self, gravity=Vector2(0, 9.8)):
        self.gravity = gravity
        self.bodies = []
        self.collision_callbacks = EventManager()
    def AddBody(self, sprite, rigidbody, shape):
        self.bodies.append((sprite, rigidbody, shape))
        sprite.body = rigidbody
    def RemoveBody(self, sprite):
        self.bodies = [b for b in self.bodies if b[0] is not sprite]
        sprite.body = None
    def Update(self, dt):
        for sprite, body, shape in self.bodies:
            if not body.is_static:
                body.integrate(dt, self.gravity)
                sprite.x += body.velocity.x * dt
                sprite.y += body.velocity.y * dt
        for i, (sprite_a, body_a, shape_a) in enumerate(self.bodies):
            for j, (sprite_b, body_b, shape_b) in enumerate(self.bodies[i+1:], i+1):
                if self._CheckCollision(sprite_a, shape_a, sprite_b, shape_b):
                    self.collision_callbacks.Emit("collision", sprite_a, sprite_b)
                    self._ResolveCollision(sprite_a, body_a, shape_a,
                                            sprite_b, body_b, shape_b)
    def _CheckCollision(self, sprite_a, shape_a, sprite_b, shape_b):
        if isinstance(shape_a, AABBShape) and isinstance(shape_b, AABBShape):
            left_a = sprite_a.x - shape_a.half_width
            right_a = sprite_a.x + shape_a.half_width
            top_a = sprite_a.y - shape_a.half_height
            bottom_a = sprite_a.y + shape_a.half_height
            left_b = sprite_b.x - shape_b.half_width
            right_b = sprite_b.x + shape_b.half_width
            top_b = sprite_b.y - shape_b.half_height
            bottom_b = sprite_b.y + shape_b.half_height
            return (left_a < right_b and right_a > left_b and
                    top_a < bottom_b and bottom_a > top_b)
        return False
    def _ResolveCollision(self, sprite_a, body_a, shape_a, sprite_b, body_b, shape_b):
        dx = sprite_a.x - sprite_b.x
        dy = sprite_a.y - sprite_b.y
        dist = m.sqrt(dx*dx + dy*dy)
        if dist == 0:
            return
        overlap = (shape_a.radius + shape_b.radius) - dist
        if overlap > 0:
            push = (dx / dist) * overlap * 0.5
            if not body_a.is_static:
                sprite_a.x += push.x
                sprite_a.y += push.y
            if not body_b.is_static:
                sprite_b.x -= push.x
                sprite_b.y -= push.y

class Scene:
    def OnEnter(self):
        pass
    def OnExit(self):
        pass
    def Update(self, dt):
        pass
    def Draw(self):
        pass

class SceneManager:
    def __init__(self):
        self.stack = []
    def Push(self, scene):
        self.stack.append(scene)
        scene.on_enter()
    def Pop(self):
        if self.stack:
            scene = self.stack.pop()
            scene.on_exit()
        return scene
    def Change(self, scene):
        self.Pop()
        self.Push(scene)
    def Update(self, dt):
        if self.stack:
            self.stack[-1].update(dt)
    def Draw(self):
        if self.stack:
            self.stack[-1].draw()

class Timer:
    def __init__(self):
        self._timers = []
    def SetTimeout(self, callback, delay, *args, **kwargs):
        self._timers.append((delay, callback, False, args, kwargs))
    def SetInterval(self, callback, interval, *args, **kwargs):
        self._timers.append((interval, callback, True, args, kwargs))
    def Update(self, dt):
        for i, (remaining, callback, repeating, args, kwargs) in enumerate(self._timers[:]):
            remaining -= dt
            if remaining <= 0:
                callback(*args, **kwargs)
                if repeating:
                    self._timers[i] = (self._timers[i][0] + remaining, callback, repeating, args, kwargs)
                else:
                    self._timers.pop(i)

class Camera:
    def __init__(self, x=0, y=0, zoom=1.0):
        self.x = x
        self.y = y
        self.zoom = zoom
        self.target = None
        self.smooth = 0.1
    def Follow(self, sprite, smooth=0.1):
        self.target = sprite
        self.smooth = smooth
    def Update(self, dt):
        if self.target:
            target_x = self.target.x
            target_y = self.target.y
            self.x += (target_x - self.x) * self.smooth
            self.y += (target_y - self.y) * self.smooth
    def WorldToScreen(self, world_x, world_y):
        return ((world_x - self.x) * self.zoom + rl.get_screen_width() / 2,
                (world_y - self.y) * self.zoom + rl.get_screen_height() / 2)
    def ScreenToWorld(self, screen_x, screen_y):
        return ((screen_x - rl.get_screen_width() / 2) / self.zoom + self.x,
                (screen_y - rl.get_screen_height() / 2) / self.zoom + self.y)
    def Apply(self):
        rl.begin_mode2d(rl.Camera2D(
            rl.Vector2(self.x, self.y),
            rl.Vector2(rl.get_screen_width() / 2, rl.get_screen_height() / 2),
            0, self.zoom))
    def End(self):
        rl.end_mode2d()

class Particle:
    def __init__(self, x, y, vx, vy, lifetime, color, size=2):
        self.x = x
        self.y = y
        self.vx = vx
        self.vy = vy
        self.lifetime = lifetime
        self.max_lifetime = lifetime
        self.color = color
        self.size = size
    def Update(self, dt):
        self.x += self.vx * dt
        self.y += self.vy * dt
        self.lifetime -= dt
    def Draw(self):
        if self.lifetime > 0:
            alpha = self.lifetime / self.max_lifetime
            col = (self.color[0], self.color[1], self.color[2], int(alpha * 255))
            DrawCircle(int(self.x), int(self.y), self.size, col)

class ParticleEmitter:
    def __init__(self, x, y, rate=10, lifetime=1.0, speed=50, spread=30, color=(255,255,255)):
        self.x = x
        self.y = y
        self.rate = rate
        self.lifetime = lifetime
        self.speed = speed
        self.spread = spread
        self.color = color
        self.particles = []
        self.time_accum = 0
    def Emit(self):
        angle = random.uniform(-self.spread, self.spread) * (m.pi / 180)
        vx = m.cos(angle) * self.speed
        vy = m.sin(angle) * self.speed
        p = Particle(self.x, self.y, vx, vy, self.lifetime, self.color)
        self.particles.append(p)
    def Update(self, dt):
        self.time_accum += dt
        while self.time_accum > 1.0 / self.rate:
            self.Emit()
            self.time_accum -= 1.0 / self.rate
        for p in self.particles[:]:
            p.update(dt)
            if p.lifetime <= 0:
                self.particles.remove(p)
    def draw(self):
        for p in self.particles:
            p.draw()

class AudioManager:
    def __init__(self):
        self.sounds = {}
        self.music = None
        self.master_volume = 1.0
        self.music_volume = 1.0
        self.sfx_volume = 1.0
        rl.init_audio_device()
    def LoadSound(self, name, filepath):
        sound = rl.load_sound(filepath)
        self.sounds[name] = sound
    def PlaySound(self, name, volume=None):
        if name in self.sounds:
            vol = volume if volume is not None else self.sfx_volume * self.master_volume
            rl.set_sound_volume(self.sounds[name], vol)
            rl.play_sound(self.sounds[name])
    def LoadMusic(self, filepath):
        self.music = rl.load_music_stream(filepath)
    def PlayMusic(self, loop=True):
        if self.music:
            rl.play_music_stream(self.music)
            rl.set_music_volume(self.music, self.music_volume * self.master_volume)
    def UpdateMusic(self):
        if self.music and rl.is_music_stream_playing(self.music):
            rl.update_music_stream(self.music)
        elif self.music and not rl.is_music_stream_playing(self.music):
            rl.play_music_stream(self.music)
    def SetMasterVolume(self, vol):
        self.master_volume = max(0, min(1, vol))
    def SetMusicVolume(self, vol):
        self.music_volume = max(0, min(1, vol))
        if self.music:
            rl.set_music_volume(self.music, self.music_volume * self.master_volume)
    def SetSfxVolume(self, vol):
        self.sfx_volume = max(0, min(1, vol))
    def Close(self):
        for sound in self.sounds.values():
            rl.unload_sound(sound)
        if self.music:
            rl.unload_music_stream(self.music)
        rl.close_audio_device()

class Animation:
    def __init__(self, frames, frame_duration=0.1, loop=True):
        self.frames = frames
        self.frame_duration = frame_duration
        self.loop = loop
        self.current_frame = 0
        self.time_accum = 0
        self.playing = True
    def Update(self, dt):
        if not self.playing:
            return
        self.time_accum += dt
        if self.time_accum >= self.frame_duration:
            self.time_accum = 0
            self.current_frame += 1
            if self.current_frame >= len(self.frames):
                if self.loop:
                    self.current_frame = 0
                else:
                    self.current_frame = len(self.frames) - 1
                    self.playing = False
    def GetCurrentFrame(self):
        return self.frames[self.current_frame]
    def Play(self):
        self.playing = True
    def Stop(self):
        self.playing = False
    def Reset(self):
        self.current_frame = 0
        self.time_accum = 0
        self.playing = True

class AnimatedSprite(Sprite):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.animation = None
    def SetAnimation(self, animation):
        self.animation = animation
    def UpdateAnimation(self, dt):
        if self.animation:
            self.animation.update(dt)
    def Draw(self, x=None, y=None, rot=None, scale=None, tint=None):
        if self.animation:
            tex_id = self.animation.get_current_frame()
            super().Draw(x, y, rot, scale, tex_id, tint)
        else:
            super().Draw(x, y, rot, scale, 0, tint)

class Tilemap:
    def __init__(self):
        self.tileset = None
        self.tile_width = 16
        self.tile_height = 16
        self.layers = []
        self.width = 0
        self.height = 0
    def LoadFromJson(self, filepath, texture):
        with open(filepath, 'r') as f:
            data = json.load(f)
        self.tileset = texture
        self.tile_width = data.get('tilewidth', 16)
        self.tile_height = data.get('tileheight', 16)
        self.width = data.get('width', 0)
        self.height = data.get('height', 0)
        self.layers = []
        for layer in data.get('layers', []):
            if layer.get('type') == 'tilelayer':
                self.layers.append(layer.get('data', []))
    def Draw(self, camera=None):
        if not self.tileset:
            return
        for layer in self.layers:
            for y in range(self.height):
                for x in range(self.width):
                    idx = layer[y * self.width + x]
                    if idx == 0:
                        continue
                    src_x = ((idx - 1) % (self.tileset.width // self.tile_width)) * self.tile_width
                    src_y = ((idx - 1) // (self.tileset.width // self.tile_width)) * self.tile_height
                    dest_x = x * self.tile_width
                    dest_y = y * self.tile_height
                    if camera:
                        screen_x, screen_y = camera.world_to_screen(dest_x, dest_y)
                        rl.draw_texture_rec(self.tileset,
                                            rl.Rectangle(src_x, src_y, self.tile_width, self.tile_height),
                                            rl.Vector2(screen_x, screen_y), rl.WHITE)
                    else:
                        rl.draw_texture_rec(self.tileset,
                                            rl.Rectangle(src_x, src_y, self.tile_width, self.tile_height),
                                            rl.Vector2(dest_x, dest_y), rl.WHITE)

def DeriveKeyFromPassword(password: str, salt: bytes = b'salt_123') -> bytes:
    return base64.urlsafe_b64encode(hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000, dklen=32))

def EncryptData(data, key: bytes):
    json_str = json.dumps(data)
    cipher = Fernet(key)
    encrypted = cipher.encrypt(json_str.encode('utf-8'))
    return base64.b64encode(encrypted).decode('utf-8')

def DecryptData(encrypted_b64: str, key: bytes):
    cipher = Fernet(key)
    encrypted = base64.b64decode(encrypted_b64)
    json_str = cipher.decrypt(encrypted).decode('utf-8')
    return json.loads(json_str)

def Savegame(data, filename, key: bytes):
    encrypted_b64 = EncryptData(data, key)
    with open(filename, 'w') as f:
        f.write(encrypted_b64)

def Loadgame(filename, key: bytes):
    with open(filename, 'r') as f:
        encrypted_b64 = f.read().strip()
    return DecryptData(encrypted_b64, key)

class UIElement:
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.visible = True
    def Update(self, mouse_x, mouse_y, mouse_clicked):
        pass
    def Draw(self):
        pass

class Button(UIElement):
    def __init__(self, x, y, width, height, text, callback, normal_color=(100,100,100), hover_color=(150,150,150)):
        super().__init__(x, y, width, height)
        self.text = text
        self.callback = callback
        self.normal_color = normal_color
        self.hover_color = hover_color
        self.is_hovered = False
    def Update(self, mouse_x, mouse_y, mouse_clicked):
        self.is_hovered = (self.x <= mouse_x <= self.x+self.width and
                           self.y <= mouse_y <= self.y+self.height)
        if self.is_hovered and mouse_clicked:
            self.callback()
    def Draw(self):
        color = self.hover_color if self.is_hovered else self.normal_color
        DrawRect(self.x, self.y, self.width, self.height, color)
        font_size = 20
        text_width = rl.measure_text(self.text, font_size)
        text_x = self.x + (self.width - text_width) // 2
        text_y = self.y + (self.height - font_size) // 2
        rl.draw_text(self.text, text_x, text_y, font_size, rl.BLACK)

class Label(UIElement):
    def __init__(self, x, y, text, font_size=20, color=(0,0,0)):
        super().__init__(x, y, 0, 0)
        self.text = text
        self.font_size = font_size
        self.color = color
    def Draw(self):
        rl.draw_text(self.text, self.x, self.y, self.font_size,
                     rl.Color(self.color[0], self.color[1], self.color[2], self.color[3]))

class UIManager:
    def __init__(self):
        self.elements = []
    def Add(self, element):
        self.elements.append(element)
    def Remove(self, element):
        self.elements.remove(element)
    def Update(self):
        mouse_pos = rl.get_mouse_position()
        mouse_clicked = rl.is_mouse_button_pressed(rl.MOUSE_LEFT_BUTTON)
        for elem in self.elements:
            if elem.visible:
                elem.update(mouse_pos.x, mouse_pos.y, mouse_clicked)
    def Draw(self):
        for elem in self.elements:
            if elem.visible:
                elem.draw()

class ResourceManager:
    def __init__(self):
        self.textures = {}
        self.sounds = {}
        self.music = {}
        self.fonts = {}
    def LoadTexture(self, name, filepath):
        tex = rl.load_texture(filepath)
        self.textures[name] = tex
        return tex
    def GetTexture(self, name):
        return self.textures.get(name)
    def LoadSound(self, name, filepath):
        sound = rl.load_sound(filepath)
        self.sounds[name] = sound
        return sound
    def GetSound(self, name):
        return self.sounds.get(name)
    def LoadMusic(self, name, filepath):
        music = rl.load_music_stream(filepath)
        self.music[name] = music
        return music
    def GetMusic(self, name):
        return self.music.get(name)
    def LoadFont(self, name, filepath, font_size=20):
        font = rl.load_font(filepath)
        self.fonts[name] = font
        return font
    def GetFont(self, name):
        return self.fonts.get(name)
    def UnloadAll(self):
        for tex in self.textures.values():
            rl.unload_texture(tex)
        for sound in self.sounds.values():
            rl.unload_sound(sound)
        for music in self.music.values():
            rl.unload_music_stream(music)
        for font in self.fonts.values():
            rl.unload_font(font)
        self.textures.clear()
        self.sounds.clear()
        self.music.clear()
        self.fonts.clear()

class NetworkServer:
    def __init__(self, secret: bytes, host='0.0.0.0', port=5555):
        self.host = host
        self.port = port
        self.secret = secret
        self.cipher = Fernet(secret)
        self.socket = None
        self.running = False
        self.clients = {}
        self.lock = threading.Lock()
        self.events = EventManager()
    def Start(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind((self.host, self.port))
        self.socket.listen(5)
        self.running = True
        self.accept_thread = threading.Thread(target=self._AcceptLoop, daemon=True)
        self.accept_thread.start()
        print(f"Secure server started on {self.host}:{self.port}")
    def Stop(self):
        self.running = False
        with self.lock:
            for client_socket, info in self.clients.items():
                try:
                    client_socket.close()
                except:
                    pass
            self.clients.clear()
        if self.socket:
            self.socket.close()
            self.socket = None
        print("Server stopped")
    def _AcceptLoop(self):
        while self.running:
            if self.socket is None:
                break
            try:
                client_socket, addr = self.socket.accept()
                auth_thread = threading.Thread(target=self._AuthenticateClient,
                                               args=(client_socket, addr),
                                               daemon=True)
                auth_thread.start()
            except OSError:
                break
            except Exception as e:
                print(f"Error in accept loop: {e}")
    def _AuthenticateClient(self, client_socket, addr):
        try:
            raw_len = client_socket.recv(4)
            if not raw_len:
                return
            msg_len = int.from_bytes(raw_len, byteorder='big')
            data = b''
            while len(data) < msg_len:
                chunk = client_socket.recv(msg_len - len(data))
                if not chunk:
                    return
                data += chunk
            received_secret = data.decode('utf-8')
            if received_secret == self.secret.decode('utf-8'):
                print(f"Client {addr} authenticated")
                with self.lock:
                    self.clients[client_socket] = {"addr": addr, "authenticated": True}
                handler_thread = threading.Thread(target=self._HandleClient,
                                                  args=(client_socket, addr),
                                                  daemon=True)
                handler_thread.start()
            else:
                print(f"Client {addr} failed authentication")
                client_socket.close()
        except Exception as e:
            print(f"Authentication error for {addr}: {e}")
            client_socket.close()
    def _HandleClient(self, client_socket, addr):
        try:
            while self.running:
                raw_len = client_socket.recv(4)
                if not raw_len:
                    break
                msg_len = int.from_bytes(raw_len, byteorder='big')
                encrypted = b''
                while len(encrypted) < msg_len:
                    chunk = client_socket.recv(msg_len - len(encrypted))
                    if not chunk:
                        break
                    encrypted += chunk
                if len(encrypted) != msg_len:
                    break
                try:
                    plain = self.cipher.decrypt(encrypted)
                    message = json.loads(plain.decode('utf-8'))
                except (json.JSONDecodeError, Exception) as e:
                    print(f"Decryption/JSON error from {addr}: {e}")
                    continue
                self.events.Emit("message", client_socket, addr, message)
        except Exception as e:
            print(f"Client {addr} error: {e}")
        finally:
            client_socket.close()
            with self.lock:
                if client_socket in self.clients:
                    del self.clients[client_socket]
            print(f"Client {addr} disconnected")
    def SendToClient(self, client_socket, data):
        try:
            plain = json.dumps(data).encode('utf-8')
            encrypted = self.cipher.encrypt(plain)
            client_socket.send(len(encrypted).to_bytes(4, byteorder='big') + encrypted)
        except Exception as e:
            print(f"Send error: {e}")
    def Broadcast(self, data, exclude=None):
        with self.lock:
            for client in self.clients:
                if client is exclude:
                    continue
                self.SendToClient(client, data)

class NetworkClient:
    def __init__(self, secret: bytes, host='localhost', port=5555):
        self.host = host
        self.port = port
        self.secret = secret
        self.cipher = Fernet(secret)
        self.socket = None
        self.running = False
        self.message_queue = queue.Queue()
        self.events = EventManager()
    def Connect(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            self.socket.connect((self.host, self.port))
            secret_bytes = self.secret.decode('utf-8')
            self.socket.send(len(secret_bytes).to_bytes(4, byteorder='big') + secret_bytes.encode('utf-8'))
            self.running = True
            self.receive_thread = threading.Thread(target=self._ReceiveLoop, daemon=True)
            self.receive_thread.start()
            print(f"Connected to secure server {self.host}:{self.port}")
            self.events.Emit("connected")
            return True
        except Exception as e:
            print(f"Connection failed: {e}")
            return False
    def Disconnect(self):
        self.running = False
        if self.socket:
            self.socket.close()
            self.socket = None
        self.events.Emit("disconnected")
        print("Disconnected")
    def _ReceiveLoop(self):
        while self.running:
            if self.socket is None:
                break
            try:
                raw_len = self.socket.recv(4)
                if not raw_len:
                    break
                msg_len = int.from_bytes(raw_len, byteorder='big')
                encrypted = b''
                while len(encrypted) < msg_len:
                    if self.socket is None:
                        break
                    chunk = self.socket.recv(msg_len - len(encrypted))
                    if not chunk:
                        break
                    encrypted += chunk
                if len(encrypted) != msg_len:
                    break
                try:
                    plain = self.cipher.decrypt(encrypted)
                    message = json.loads(plain.decode('utf-8'))
                except (json.JSONDecodeError, Exception) as e:
                    print(f"Decryption/JSON error: {e}")
                    continue
                self.events.Emit("message", message)
                self.message_queue.put(message)
            except Exception as e:
                if self.running:
                    print(f"Receive error: {e}")
                break
        self.running = False
        if self.socket:
            self.socket.close()
            self.socket = None
        self.events.Emit("disconnected")
    def Send(self, data):
        if not self.running or self.socket is None:
            print("Client not connected")
            return
        try:
            plain = json.dumps(data).encode('utf-8')
            encrypted = self.cipher.encrypt(plain)
            self.socket.send(len(encrypted).to_bytes(4, byteorder='big') + encrypted)
        except Exception as e:
            print(f"Send error: {e}")
    def PollMessages(self):
        messages = []
        while True:
            try:
                messages.append(self.message_queue.get_nowait())
            except queue.Empty:
                break
        return messages