import gzip
import json
import os
import socket
import threading
import uuid
from time import sleep

import netifaces as ni
import pyperclip
from lru import LRU

PORT = int(os.environ.get("PY_HANDOFF_PORT", 5005))
KEY = os.environ.get("PY_HANDOFF_KEY", "D0AA67DD-C285-45A2-B7A7-F5277F613E3C")
MAX_DGRAM_RECV_BUFF_SIZE = int(os.environ.get("PY_HANDOFF_RECV_BUFF_SIZE", 1500))

self_id = uuid.uuid4().hex
incoming_clip = LRU(5)


def obfuscate(byt):
    # Use same function in both directions.  Input and output are bytes
    # objects.
    mask = KEY.encode()
    lmask = len(mask)
    return bytes(c ^ mask[i % lmask] for i, c in enumerate(byt))


def encode_msg(msg):
    msg = json.dumps(
        {
            "msg": msg,
            "from": self_id,
        }
    )
    msg = msg.encode()
    msg = gzip.compress(msg, compresslevel=9)
    msg = obfuscate(msg)
    return msg


def decode_msg(data):
    try:
        data = obfuscate(data)
        data = gzip.decompress(data)
        data = data.decode()
        data = json.loads(data)
        return data["msg"], data["from"]
    except Exception as e:
        print(e)
        return None, None


def waitForNewPaste():
    originalText = pyperclip.paste()
    while True:
        currentText = pyperclip.paste()
        if currentText != originalText:
            return currentText
        sleep(1)


def server():
    """Broadcasting clipboard changes to LAN"""
    while True:
        msg = waitForNewPaste()
        if msg in incoming_clip:
            # if the msg is from other nodes
            # do not broadcast again
            continue
        interfaces = ni.interfaces()
        msg = encode_msg(msg)
        for interface in interfaces:
            ifaddress = ni.ifaddresses(interface)
            if not ifaddress or 2 not in ifaddress:
                continue
            try:
                ip = ifaddress[2][0]["addr"]
                sock = socket.socket(
                    socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP
                )  # UDP
                sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
                sock.bind((ip, 0))

                sock.sendto(msg, ("255.255.255.255", PORT))
                sock.close()
            except Exception as e:
                print(
                    f"server occurred exception when broadcasting on {ip}: {type(e)} - {e};",
                    end="\t",
                )
                print(f"Message size: {len(msg)} bytes")
            else:
                print("server broadcasted on", ip)


def client():
    """Subscribing clipboard changes from remote, and updating to local"""
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    sock.bind(("0.0.0.0", PORT))
    while True:
        data, addr = sock.recvfrom(MAX_DGRAM_RECV_BUFF_SIZE)
        msg, from_id = decode_msg(data)
        if msg is not None and from_id != self_id:
            print(f"client received packet from {addr};", end="\t")
            print(f"Message size: {len(msg)} bytes")
            incoming_clip[msg] = True
            try:
                pyperclip.copy(msg)
            except Exception as e:
                print(e)


def entrypoint():
    server_thread = threading.Thread(target=server)
    server_thread.start()
    client()
