#!/usr/bin/env python3
"""
Stego GUI v3 (fără header) — afișează lungimea mesajului
- Embed: citește 'datebinare.txt', cere mesaj + cheie, scrie 'datecriptate.txt' (NUMAI 0/1).
- Extract: citește 'datecriptate.txt', cere cheie + lungime mesaj (caractere sau biți) și afișează mesajul.
- Afișare live: la tastare, arată lungimea mesajului în caractere UTF-8 (bytes) și în biți.
"""
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import hashlib, os

# ---------- utilitare bit/byte ----------
def bytes_to_bits(b: bytes):
    out = []
    for byte in b:
        for i in range(8):
            out.append((byte >> (7-i)) & 1)
    return out

def bits_to_bytes(bits):
    extra = (-len(bits)) % 8
    if extra:
        bits = bits + [0]*extra
    out = bytearray()
    for i in range(0, len(bits), 8):
        byte = 0
        for j in range(8):
            byte = (byte << 1) | bits[i+j]
        out.append(byte)
    return bytes(out)

def str_to_bits(s: str, encoding='utf-8'):
    return bytes_to_bits(s.encode(encoding))

def bits_to_str(bits, encoding='utf-8'):
    b = bits_to_bytes(bits)
    try:
        return b.rstrip(b'\x00').decode(encoding)
    except Exception:
        return b.decode(encoding, errors='replace')

# ---------- keystream + poziții ----------
def sha256_stream(key: bytes, nbits: int):
    out_bits = []
    counter = 0
    while len(out_bits) < nbits:
        h = hashlib.sha256()
        h.update(key)
        h.update(counter.to_bytes(8,'big'))
        out_bits.extend(bytes_to_bits(h.digest()))
        counter += 1
    return out_bits[:nbits]

def deterministic_sample_positions(key: bytes, n: int, k: int):
    if k > n:
        raise ValueError("k > n")
    idx = list(range(n))
    ctr = 0
    for i in range(n-1, n-k-1, -1):
        h = hashlib.sha256()
        h.update(key); h.update(ctr.to_bytes(8,'big'))
        rnd = int.from_bytes(h.digest()[:8],'big')
        j = rnd % (i+1)
        idx[i], idx[j] = idx[j], idx[i]
        ctr += 1
    return idx[-k:]

# ---------- embed/extract ----------
def embed(noise_bits, message_text, key):
    n = len(noise_bits)
    msg_bits = str_to_bits(message_text)
    m = len(msg_bits)
    if m == 0:
        raise ValueError("Mesaj gol.")
    if m > n:
        raise ValueError("Mesajul e prea lung pentru fișierul de zgomot.")
    keyb = key.encode('utf-8')
    ks = sha256_stream(keyb + b'::ks::msg', m)
    payload = [mb ^ kb for mb,kb in zip(msg_bits, ks)]
    pos = deterministic_sample_positions(keyb + b'::pos', n, m)
    stego = noise_bits.copy()
    for p, bit in zip(pos, payload):
        stego[p] = bit  # REPLACE
    return stego

def extract(stego_bits, key, m_bits):
    n = len(stego_bits)
    if m_bits > n:
        raise ValueError("m_bits > n")
    keyb = key.encode('utf-8')
    pos = deterministic_sample_positions(keyb + b'::pos', n, m_bits)
    payload = [stego_bits[p] for p in pos]
    ks = sha256_stream(keyb + b'::ks::msg', m_bits)
    msg_bits = [p ^ k for p,k in zip(payload, ks)]
    return bits_to_str(msg_bits)

# ---------- file helpers ----------
def read_bitfile(path):
    with open(path,'r') as f:
        s = f.read().strip()
    if any(c not in '01' for c in s):
        raise ValueError("Fișierul trebuie să conțină doar 0 și 1.")
    return [int(c) for c in s]

def write_bitfile(path, bits):
    with open(path,'w') as f:
        f.write(''.join(str(b) for b in bits))

# ---------- GUI ----------
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Stego v3 (fără header) — lungime mesaj")
        self.geometry("780x600")
        # EMBED
        fe = tk.LabelFrame(self, text="Embed (ascunde mesaj)", padx=8, pady=8)
        fe.pack(fill='x', padx=8, pady=6)
        tk.Label(fe, text="Fișier zgomot (implicit 'datebinare.txt'):").grid(row=0, column=0, sticky='w')
        self.ent_noise = tk.Entry(fe, width=60); self.ent_noise.grid(row=0, column=1, padx=6); self.ent_noise.insert(0,"datebinare.txt")
        tk.Button(fe, text="Browse", command=self.pick_noise).grid(row=0, column=2)
        tk.Label(fe, text="Cheie:").grid(row=1, column=0, sticky='w')
        self.ent_key_e = tk.Entry(fe, width=40, show='*'); self.ent_key_e.grid(row=1, column=1, sticky='w')
        tk.Label(fe, text="Mesaj:").grid(row=2, column=0, sticky='nw')
        self.txt_msg = scrolledtext.ScrolledText(fe, width=60, height=6); self.txt_msg.grid(row=2, column=1, columnspan=2, pady=6)
        # Lungime mesaj
        self.var_len = tk.StringVar(value="Lungime mesaj: 0 bytes (0 biți)")
        self.lbl_len = tk.Label(fe, textvariable=self.var_len); self.lbl_len.grid(row=3, column=1, sticky='w')
        tk.Button(fe, text="Generează 'datecriptate.txt'", command=self.do_embed).grid(row=4, column=1, sticky='w', pady=4)

        # EXTRACT
        fd = tk.LabelFrame(self, text="Extract (recuperează mesaj)", padx=8, pady=8)
        fd.pack(fill='x', padx=8, pady=6)
        tk.Label(fd, text="Fișier stego (implicit 'datecriptate.txt'):").grid(row=0, column=0, sticky='w')
        self.ent_stego = tk.Entry(fd, width=60); self.ent_stego.grid(row=0, column=1, padx=6); self.ent_stego.insert(0,"datecriptate.txt")
        tk.Button(fd, text="Browse", command=self.pick_stego).grid(row=0, column=2)
        tk.Label(fd, text="Cheie:").grid(row=1, column=0, sticky='w')
        self.ent_key_d = tk.Entry(fd, width=40, show='*'); self.ent_key_d.grid(row=1, column=1, sticky='w')

        self.len_mode = tk.StringVar(value='chars')
        fr_len = tk.Frame(fd); fr_len.grid(row=2, column=0, columnspan=3, sticky='w')
        tk.Radiobutton(fr_len, text="Lungime mesaj în caractere", variable=self.len_mode, value='chars').pack(side='left')
        tk.Radiobutton(fr_len, text="Lungime mesaj în biți", variable=self.len_mode, value='bits').pack(side='left')

        tk.Label(fd, text="Valoare lungime:").grid(row=3, column=0, sticky='w')
        self.ent_len = tk.Entry(fd, width=20); self.ent_len.grid(row=3, column=1, sticky='w')
        tk.Button(fd, text="Extrage mesaj", command=self.do_extract).grid(row=4, column=1, sticky='w', pady=4)

        # LOG
        tk.Label(self, text="Ieșire / Log:").pack(anchor='w', padx=8)
        self.logbox = scrolledtext.ScrolledText(self, width=92, height=12); self.logbox.pack(padx=8, pady=6)

        # bind pentru actualizare lungime mesaj
        self.txt_msg.bind('<KeyRelease>', self.update_len)
        # și pentru event paste cu mouse
        self.txt_msg.bind('<<Paste>>', self.update_len)
        # init calc
        self.update_len()

    def log(self, s):
        self.logbox.insert('end', s + "\n")
        self.logbox.see('end')

    def pick_noise(self):
        p = filedialog.askopenfilename(title="Alege datebinare", filetypes=[("Text","*.txt"),("All","*.*")])
        if p: self.ent_noise.delete(0,'end'); self.ent_noise.insert(0,p)

    def pick_stego(self):
        p = filedialog.askopenfilename(title="Alege datecriptate", filetypes=[("Text","*.txt"),("All","*.*")])
        if p: self.ent_stego.delete(0,'end'); self.ent_stego.insert(0,p)

    def update_len(self, event=None):
        msg = self.txt_msg.get('1.0','end').rstrip('\n')
        b = msg.encode('utf-8')
        self.var_len.set(f"Lungime mesaj: {len(b)} bytes ({len(b)*8} biți)")

    def do_embed(self):
        noise_path = self.ent_noise.get().strip() or "datebinare.txt"
        key = self.ent_key_e.get().strip()
        msg = self.txt_msg.get('1.0','end').strip()
        if not os.path.exists(noise_path):
            messagebox.showerror("Eroare", f"Fișier inexistent: {noise_path}"); return
        if not key:
            messagebox.showerror("Eroare", "Introduceți cheia."); return
        if not msg:
            messagebox.showerror("Eroare", "Introduceți mesajul."); return
        try:
            noise_bits = read_bitfile(noise_path)
            # verificare capacitate
            m_bits = len(msg.encode('utf-8')) * 8
            if m_bits > len(noise_bits):
                raise ValueError(f"Mesaj prea lung pentru fișierul de zgomot: {m_bits} biți > {len(noise_bits)} biți.")
            stego_bits = embed(noise_bits, msg, key)
            write_bitfile("datecriptate.txt", stego_bits)
        except Exception as e:
            messagebox.showerror("Eroare la embed", str(e)); return
        self.log(f"Embed OK -> datecriptate.txt (fără header). Mesaj: {len(msg.encode('utf-8'))} bytes / {len(msg.encode('utf-8'))*8} biți.")
        messagebox.showinfo("Succes", "Fișierul 'datecriptate.txt' a fost generat.")

    def do_extract(self):
        stego_path = self.ent_stego.get().strip() or "datecriptate.txt"
        key = self.ent_key_d.get().strip()
        if not os.path.exists(stego_path):
            messagebox.showerror("Eroare", f"Fișier inexistent: {stego_path}"); return
        if not key:
            messagebox.showerror("Eroare", "Introduceți cheia."); return
        mode = self.len_mode.get()
        try:
            val = int(self.ent_len.get().strip())
        except Exception:
            messagebox.showerror("Eroare", "Introduceți o valoare întreagă pentru lungime."); return
        try:
            stego_bits = read_bitfile(stego_path)
            m_bits = val if mode=='bits' else val*8
            msg = extract(stego_bits, key, m_bits)
        except Exception as e:
            messagebox.showerror("Eroare la extragere", str(e)); return
        self.log("Mesaj extras:")
        self.log(msg)
        messagebox.showinfo("Mesaj extras", msg)

if __name__ == '__main__':
    App().mainloop()
