# AGPLv3 or later

import json
import zlib

def encode(data, send_raw=False):

    # This will encode data for sending.
    # Data should be a writable into JSON.

    if not send_raw:
        J = json.dumps(data).encode("utf-8")
    else:
        J = data.encode("utf-8")
    C = zlib.compress(J)

    zed = len(C)
    raw = len(J)

    # Sometime compressed is larger than raw.
    # We will add a byte saying 69 if it comressed
    # and a byte saying 0 if it not sexy like this.

    if not send_raw and zed < raw:
        return chr(69).encode("utf-8")  +  C
    else:
        return chr(0).encode("utf-8")   +  J

def decode(data):

    # This will decode the data

    try:
        code = data[0]
        data = data[1:]
    except:
        code = 0


    # 69 means the data is sexy
    # see encode() for more info
        
    if code == 69:       
        data = zlib.decompress(data).decode()
    else:
        data = data.decode()

    # It should be Json but it could be something else
        
    try:
        return json.loads(data)
    except:
        return data
    

cache  = {}
cache2 = {}

def diff(d1, d2, CHANGED=True):

    # Function that checkes for differences
    # between two dictionaries

    ans = {}

    # Comparing dictionaries
    for i in d1:
        if d1[i] != d2.get(i) and i in d2:
            if not type(d2.get(i)) == dict:
                ans[i] = d2[i]
            else:
                try:
                    ans[i] = diff(d1[i], d2[i], CHANGED=False)
                except:
                    ans[i] = d2[i]

    # Adding in new stuff
    for i in d2:
        if i not in d1:
            ans[i] = d2[i]

    # Telling it to remove old stuff
    remove = []
    for i in d1:
        if i not in d2:
            remove.append(i)
    if remove:
        ans["DELETED"] = remove

    if ans and CHANGED:
        ans["CHANGED"] = True
    return ans

def combine(d1,d2):

    # Function that combines 2 dictionaries
    # based on the diff version.

    # Changed parts
    for i in d1:
        if d1[i] != d2.get(i) and i in d2:
            if not type(d2.get(i)) == dict:
                d1[i] = d2[i]
            else:
                try:
                    combine(d1[i], d2[i])
                except:
                    d1[i] = d2[i]

    # Add new stuff

    for i in d2:
        
        if i in ("DELETED", "CHANGED"):
            continue
        
        if i not in d1:
            d1[i] = d2[i]

    # Deleting stuff

    if "DELETED" in d2:
        for i in d2["DELETED"]:
            try:
                del d1[i]
            except:
                pass
    
def encodeObj(data, ID, cache=cache):

    # encodes Object, stripping it from all data
    # that is unecessary

    if ID not in cache:
        cache[ID] = data
        return encode(data)

    else:
        D = diff(cache[ID], data)
        cache[ID] = data
        return encode(D)

def decodeObj(data, ID, cache=cache):
    data = decode(data)
    if ID not in cache:

        # TODO: implement the check to see
        # if the data is actually CHANGED
        # or not. And send back the request for the
        # full thing if it is only the part.
        
        cache[ID] = data
        return data
    else:
        combine(cache[ID], data)
        return cache[ID]