#
# PyPacker v1.0
#
# Single-File Directory Compression/Decompression Script (Using a bad custom format, because why not)
# https://alignedcookie88.com/files/pypacker.py
#
# Licensed under The Unlicense (see https://choosealicense.com/licenses/unlicense/)
# Credit is optional, but would be greatly appreciated
#


DEBUG_DONT_USE_COMPRESSION = False


import gzip
import os
import io

def uint_to_bytes(i: int) -> bytes:
    return i.to_bytes(4, "little", signed=False)

def bytes_to_uint(i: bytes) -> int:
    return int.from_bytes(i, "little", signed=False)

def read_uint(readable: io.BytesIO) -> int:
    return bytes_to_uint(readable.read(4))


def length_prefix_string(string: str) -> bytes:
    d = string.encode("utf-8")
    return uint_to_bytes(len(d)) + d

def read_length_prefixed_string(readable: io.BytesIO) -> str:
    length = read_uint(readable)
    if length == 0:
        return None
    d = readable.read(length)
    return d.decode("utf-8")


class CompressException(Exception):
    pass

def compress_directory(directory: str, output_file: str):
    if not os.path.isdir(directory):
        raise CompressException("The directory supplied either does not exist or is a file.")
    if os.path.exists(output_file):
        raise CompressException("The output file supplied already exists.")
    
    if DEBUG_DONT_USE_COMPRESSION:
        out = open(output_file, "xb")
    else:
        out = gzip.open(output_file, "xb")

    for root, _, files in os.walk(directory):
        for name in files:

            filename = os.path.join(root, name)
            f = open(filename, "rb")
            name = filename.removeprefix(directory).removeprefix("\\").removeprefix("/").replace("\\", "/") # bad code ik

            out.write(length_prefix_string(name))

            print(f"Compressing {name} ({root})")

            # Read 1024-byte sized buffers
            while True:
                d = f.read(1024)
                l = len(d)
                out.write(uint_to_bytes(l))
                if l == 0:
                    break # Reached the end of the data, don't bother writing any extra data to the file, as when reading 0-byte blocks marks the end of the entry
                out.write(d)

            f.close()
    
    out.write(length_prefix_string("")) # An empty entry name tells the decoder to stop reading
    out.close()


class DecompressException(Exception):
    pass

def decompress_file(file: str, output_directory: str):
    if not os.path.isfile(file):
        raise DecompressException("The input file either does not exist or is a directory.")
    if os.path.exists(output_directory):
        raise DecompressException("The output directory already exists.")
    
    if DEBUG_DONT_USE_COMPRESSION:
        ppdc = open(file, "rb")
    else:
        ppdc = gzip.open(file, "rb")

    while True:
        name = read_length_prefixed_string(ppdc)
        if name == None or len(name) == 0: # End of file
            break

        print(f"Decompressing {name}")

        filename = os.path.join(output_directory, name)
        dirname = os.path.dirname(filename)
        os.makedirs(dirname, exist_ok=True)
        f = open(filename, "xb")
        while True:
            l = read_uint(ppdc)
            if l == 0: # End of entry
                break
            d = ppdc.read(l)
            f.write(d)
        f.close()

    ppdc.close()


if __name__ == "__main__":
    #
    # Bad GUI
    # If you are an even slightly experienced programmer, I would suggest you avert your eyes. This code is a mess.
    #

    import tkinter as tk
    import tkinter.filedialog as fd
    import tkinter.messagebox as mb

    root = tk.Tk()
    root.title("PyPacker v1.0")
    root.resizable(False, False)

    FONT = ("Arial", 25)
    WIDTH = 25
    PADDING = 10

    def gui_unpack():
        file = fd.askopenfilename(filetypes=[("PyPacker Directory Container", ".ppdc"), ("All Files", ".*")])
        if file == None:
            return
        
        dir = file.removesuffix(".ppdc")+"-unpacked"
        while True:
            if not os.path.exists(dir):
                break
            dir += "-new"
        
        try:
            decompress_file(file, dir)
            if os.name == "nt":
                os.startfile(dir) # Open directory on windows
            else: # Not supported elsewhere
                mb.showinfo(title="PyPacker", message=f"File decompressed to {dir}.")
        except Exception as e:
            mb.showerror(title="PyPacker Error", message=f"Error of type {type(e).__name__}:\n{e.args[0]}")

    def gui_pack():
        dir = fd.askdirectory()
        if dir == None:
            return
        
        file = fd.asksaveasfilename(filetypes=[("PyPacker Directory Container", ".ppdc")])
        if file == None:
            return
        if not file.endswith(".ppdc"):
            file += ".ppdc"

        try:
            compress_directory(dir, file)
            mb.showinfo(title="PyPacker", message="Folder compressed!")
        except Exception as e:
            mb.showerror(title="PyPacker Error", message=f"Error of type {type(e).__name__}:\n{e.args[0]}")
    
    btn_unpack = tk.Button(root, text="Unpack Archive", font=FONT, width=WIDTH, bg="#77FF77", command=gui_unpack)
    btn_unpack.pack(padx=PADDING, pady=PADDING)

    btn_pack = tk.Button(root, text="Pack Folder", font=FONT, width=WIDTH, bg="#FF7777", command=gui_pack)
    btn_pack.pack(padx=PADDING, pady=PADDING)

    root.mainloop()