#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Analiză avansată aleatoritate (versiune cu concluzii clare în limba română)
- Teste incluse (subset avansat, potrivit pentru fișiere mari):
  1) Entropie Shannon
  2) Test frecvență (monobit) — chi2 + p-value
  3) Test block-frequency (b paramabil)
  4) Runs test (testul runurilor)
  5) Longest run of ones (Longest run test, estimare)
  6) Serial / pattern frequency (m variabil, evaluate p-value chi2)
  7) Approximate entropy (ApEn)
  8) Lempel–Ziv complexity (estimare streaming-friendly)
  9) Autocorelație (până la lag paramabil) + concluzie pe detectare periodicitate
 10) FFT spectral peak detection (posibile periodicități)
 11) Cumulative sums (CUSUM) forward/backward
 12) Maurer Universal proxy: raport prin compresibilitate (zlib) + test statistic heuristic
 13) Teste de compresibilitate (zlib) și raport
- Ieșire: raport text (română) și log în GUI; posibilitate de salvare raport complet.
- Proiectat pentru fișiere mari: folosește streaming/segmentare când e posibil; permite analiză pe eșantion (first N biți) sau pe întreg fișierul.
- Autor: ChatGPT — adaptat la cerința utilizatorului (concluzii clare, limbă română).

Rulați local: python3 analysis_randomness_advanced.py
"""
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import numpy as np, math, zlib, os, sys, statistics
from collections import Counter
import matplotlib.pyplot as plt
from scipy import stats
import time

# ---------- helperi ----------
def read_bitfile_stream(path, max_bits=None):
    """Citește fișier text de 0/1 în streaming; returnează numpy array de tip uint8.
       Dacă max_bits specificat, se oprește după max_bits biți (utile pentru fișiere foarte mari)."""
    bits = []
    with open(path, 'r') as f:
        for line in f:
            line = line.strip()
            for ch in line:
                if ch == '0' or ch == '1':
                    bits.append(1 if ch=='1' else 0)
                    if max_bits and len(bits) >= max_bits:
                        return np.frombuffer(bytearray(bits), dtype=np.uint8)
    return np.frombuffer(bytearray(bits), dtype=np.uint8)

def shannon_entropy(bits):
    p1 = float(bits.mean()) if len(bits)>0 else 0.0
    p0 = 1.0 - p1
    ent = 0.0
    for p in (p0,p1):
        if p>0:
            ent -= p * math.log2(p)
    return ent

def chi2_monobit(bits):
    n = len(bits)
    ones = int(bits.sum())
    zeros = n - ones
    expected = n/2
    chi2 = (ones-expected)**2/expected + (zeros-expected)**2/expected
    # p-value for chi2 with df=1
    p = 1 - stats.chi2.cdf(chi2, df=1)
    return chi2, p, ones, zeros

def runs_test(bits):
    n = len(bits)
    pi = float(bits.mean())
    # prereq:
    if abs(pi - 0.5) > (2/math.sqrt(n)):
        return {'ok':False, 'reason':'Proporţia de 1 nu este aproape de 0.5 (prerechizit test).'}
    runs = 1 + int(np.sum(bits[1:] != bits[:-1]))
    expected = 2*n*pi*(1-pi) + 1
    var = (2*n*pi*(1-pi)*(2*n*pi*(1-pi)-1))/(n-1) if n>1 else 0.0
    z = (runs - expected)/math.sqrt(var) if var>0 else float('nan')
    p = 2*(1 - stats.norm.cdf(abs(z)))
    return {'runs':runs, 'expected':expected, 'z':z, 'p':p, 'ok': p>0.01}

def block_frequency(bits, block_size=128):
    n = len(bits)
    nb = n // block_size
    if nb == 0:
        raise ValueError("Bloc prea mare pentru lungimea secvenței.")
    props = []
    for i in range(nb):
        blk = bits[i*block_size:(i+1)*block_size]
        props.append(np.sum(blk)/block_size)
    props = np.array(props)
    stat = 4 * block_size * np.sum((props - 0.5)**2)
    p = 1 - stats.chi2.cdf(stat, df=nb-1)
    return {'stat':stat, 'p':p, 'nb_blocks':nb, 'mean_prop':float(props.mean())}

def longest_run_ones(bits):
    # conform NIST-like estimation for longest run of ones in a block; aici întoarcem cel mai lung run general
    maxrun = 0; cur = 0
    for b in bits:
        if b==1:
            cur += 1
            if cur>maxrun: maxrun=cur
        else:
            cur = 0
    return maxrun

def serial_test(bits, m=4):
    n = len(bits)
    if n < m+1:
        raise ValueError("Secvență prea scurtă pentru m ales.")
    counts = Counter()
    total = n - m + 1
    s = ''.join('1' if x else '0' for x in bits)
    for i in range(total):
        pat = s[i:i+m]
        counts[pat] += 1
    expected = total / (2**m)
    chi2 = sum((c-expected)**2/expected for c in counts.values())
    p = 1 - stats.chi2.cdf(chi2, df=(2**m - 1))
    return {'m':m, 'chi2':chi2, 'p':p, 'distinct':len(counts)}

def approx_entropy(bits, m=2):
    # ApEn: - (phi(m+1) - phi(m))
    n = len(bits)
    def phi(m):
        counts = Counter()
        total = n - m + 1
        s = ''.join('1' if x else '0' for x in bits)
        for i in range(total):
            counts[s[i:i+m]] += 1
        summ = 0.0
        for c in counts.values():
            p = c/total
            summ += p * math.log(p)
        return summ / total
    try:
        return - (phi(m+1) - phi(m))
    except Exception:
        return float('nan')

def lz_complexity_estimate(bits):
    # streaming-friendly LZ78-like estimation using incremental parsing
    s = ''.join('1' if x else '0' for x in bits)
    i = 0; n = len(s)
    phrases = set()
    count = 0
    while i < n:
        l = 1
        while i+l <= n and s[i:i+l] in phrases:
            l += 1
        phrases.add(s[i:i+l])
        count += 1
        i += l
    return count

def autocorr(bits, maxlag=1024):
    n = len(bits)
    x = bits - bits.mean()
    var = np.var(x)
    if var == 0: return np.zeros(0)
    ac = [np.sum(x[:-lag]*x[lag:])/(n-lag)/var for lag in range(1, min(maxlag, n-1)+1)]
    return np.array(ac)

def fft_peaks(bits, threshold_std=6):
    x = 2*bits - 1
    n = len(x)
    X = np.fft.rfft(x)
    mag = np.abs(X)
    freqs = np.fft.rfftfreq(n)
    thresh = mag.mean() + threshold_std * mag.std()
    peaks = [(freqs[i], mag[i]) for i in range(1,len(mag)) if mag[i] > thresh]
    peaks.sort(key=lambda t: t[1], reverse=True)
    return peaks, freqs, mag

def cusum(bits):
    s = 2*bits - 1
    csum = np.cumsum(s)
    z = np.max(np.abs(csum))/math.sqrt(len(s))
    p = 2*(1 - stats.norm.cdf(z))
    return {'z':z, 'p':p, 'cumsum':csum}

def compression_ratio(bits):
    # build bytes stream then compress
    n = len(bits)
    if n < 8: return 1.0
    b = bytearray()
    for i in range(0, n - (n%8), 8):
        byte = 0
        for j in range(8):
            byte = (byte << 1) | int(bits[i+j])
        b.append(byte)
    comp = zlib.compress(bytes(b))
    return len(comp) / len(b) if len(b)>0 else 1.0

# ---------- concluzii helper ----------
def verdict_from_p(p, alpha=0.01):
    return 'trece' if p >= alpha else 'respins'

def short_explain(testname, result):
    """Returnează o concluzie scurtă, clară în română pentru fiecare test."""
    if testname == 'entropy':
        ent = result
        if ent > 0.999:
            return f"Entropie Shannon = {ent:.6f} (foarte aproape de 1 → comportament aproape perfect aleator)"
        elif ent > 0.99:
            return f"Entropie Shannon = {ent:.6f} (înaltă, mică deviere de aleator)"
        else:
            return f"Entropie Shannon = {ent:.6f} (posibilă structură; nu e complet aleator)"
    if testname == 'monobit':
        chi2,p,ones,zeros = result
        v = verdict_from_p(p)
        return f"Monobit: ones={ones}, zeros={zeros}, chi2={chi2:.4f}, p={p:.3e} → verdict: {v} (alpha=0.01)"
    if testname == 'runs':
        if not result.get('ok',True):
            return "Runs test: precondiție eșuată; proporția de 1 nu e aproape 0.5."
        return f"Runs: runs={result['runs']}, z={result['z']:.3f}, p={result['p']:.3e} → verdict: { 'trece' if result['p']>=0.01 else 'respins'}"
    if testname == 'block':
        return f"Block-frequency: stat={result['stat']:.3f}, p={result['p']:.3e}, blocuri={result['nb_blocks']} → verdict: {verdict_from_p(result['p'])}"
    if testname == 'serial':
        return f"Serial (m={result['m']}): chi2={result['chi2']:.3f}, p={result['p']:.3e} → verdict: {verdict_from_p(result['p'])}"
    if testname == 'apen':
        return f"Approximate Entropy (m=2): {result:.6f} (valori mici sugerează predictibilitate)"
    if testname == 'lz':
        return f"Lempel-Ziv complexity (est): {result} (valori mari → complexitate mare)"
    if testname == 'longest_run':
        return f"Cel mai lung șir de 1 consec: {result} biți (comparați cu valorile așteptate NIST pentru n)"
    if testname == 'autocorr':
        ac = result
        high = np.max(np.abs(ac)) if len(ac)>0 else 0.0
        return f"Autocorelație maximă (prime laguri): {high:.6f} (valori mari pot indica periodicitate)"
    if testname == 'fft':
        peaks = result
        return f"FFT: {len(peaks)} vârfuri semnificative detectate (dacă >0 → periodicități posibile)"
    if testname == 'cusum':
        return f"CUSUM z={result['z']:.4f}, p~{result['p']:.3e} (verdict: {verdict_from_p(result['p'])})"
    if testname == 'compress':
        return f"Raport compresie (zlib) = {result:.6f} (valori apropiate de 1 → dificil de comprimat → mai aleator)"
    return ""

# ---------- GUI ----------
class AdvAnalyzer(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Analiză avansată aleatoritate — Raport clar (română)")
        self.geometry("980x720")
        frm = tk.Frame(self); frm.pack(fill='x', padx=6, pady=6)
        tk.Label(frm, text="Fișier (implicit datecriptate.txt):").grid(row=0,column=0,sticky='w')
        self.ent_file = tk.Entry(frm, width=80); self.ent_file.grid(row=0,column=1); self.ent_file.insert(0,"datecriptate.txt")
        tk.Button(frm, text="Browse", command=self.browse).grid(row=0,column=2, padx=4)
        tk.Label(frm, text="Max biți de analizat (0 = tot fișierul):").grid(row=1,column=0,sticky='w')
        self.ent_max = tk.Entry(frm, width=15); self.ent_max.grid(row=1,column=1,sticky='w'); self.ent_max.insert(0,"0")
        tk.Label(frm, text="Block-size (block-frequency, ex: 128):").grid(row=2,column=0,sticky='w')
        self.ent_block = tk.Entry(frm, width=15); self.ent_block.grid(row=2,column=1,sticky='w'); self.ent_block.insert(0,"128")
        tk.Label(frm, text="Serial m (pattern length, ex: 4):").grid(row=3,column=0,sticky='w')
        self.ent_m = tk.Entry(frm, width=15); self.ent_m.grid(row=3,column=1,sticky='w'); self.ent_m.insert(0,"4")
        tk.Label(frm, text="Autocorr max lag (ex: 1024):").grid(row=4,column=0,sticky='w')
        self.ent_lag = tk.Entry(frm, width=15); self.ent_lag.grid(row=4,column=1,sticky='w'); self.ent_lag.insert(0,"1024")
        tk.Button(frm, text="Rulează analize avansate și generează raport", bg='#b3ffb3', command=self.run).grid(row=5,column=1, pady=6, sticky='w')
        tk.Button(frm, text="Salvează raport ca .txt", command=self.save_report).grid(row=5,column=2, pady=6, sticky='w')

        tk.Label(self, text="Rezultate și concluzii (limba română):").pack(anchor='w', padx=6)
        self.txt = scrolledtext.ScrolledText(self, width=115, height=35); self.txt.pack(padx=6, pady=6)
        self.report_text = ""

    def browse(self):
        p = filedialog.askopenfilename(title="Alege fișier", filetypes=[("Text","*.txt"),("All","*.*")])
        if p: self.ent_file.delete(0,'end'); self.ent_file.insert(0,p)

    def append(self, s):
        self.txt.insert('end', s + "\n"); self.txt.see('end')

    def save_report(self):
        if not self.report_text:
            messagebox.showinfo("Info", "Mai întâi rulează analizele pentru a genera raportul.")
            return
        p = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("Text","*.txt")])
        if p:
            with open(p,'w', encoding='utf-8') as f:
                f.write(self.report_text)
            messagebox.showinfo("Salvat", f"Raport salvat: {p}")

    def run(self):
        path = self.ent_file.get().strip() or "datecriptate.txt"
        if not os.path.exists(path):
            messagebox.showerror("Eroare", f"Fișierul nu există: {path}"); return
        try:
            maxb = int(self.ent_max.get().strip())
            block = int(self.ent_block.get().strip())
            m = int(self.ent_m.get().strip())
            lag = int(self.ent_lag.get().strip())
        except Exception as e:
            messagebox.showerror("Eroare param", str(e)); return

        t0 = time.time()
        self.append(f"Încep analiza pe fișier: {path} (max biți={maxb if maxb>0 else 'toate'})")
        bits = read_bitfile_stream(path, max_bits=maxb if maxb>0 else None)
        n = len(bits)
        if n==0:
            messagebox.showerror("Eroare", "Fișier gol sau conținut invalid."); return
        self.append(f"Biți citiți: {n}")

        # run tests
        ent = shannon_entropy(bits); self.append(short_explain('entropy', ent))
        chi2,p,ones,zeros = chi2_monobit(bits); self.append(short_explain('monobit', (chi2,p,ones,zeros)))
        runs = runs_test(bits); self.append(short_explain('runs', runs))
        try:
            bf = block_frequency(bits, block_size=block); self.append(short_explain('block', bf))
        except Exception as e:
            bf = None; self.append(f"Block-frequency: eroare {e}")
        try:
            ser = serial_test(bits, m=m); self.append(short_explain('serial', ser))
        except Exception as e:
            ser = None; self.append(f"Serial test: eroare {e}")
        apen = approx_entropy(bits, m=2); self.append(short_explain('apen', apen))
        lz = lz_complexity_estimate(bits); self.append(short_explain('lz', lz))
        lr = longest_run_ones(bits); self.append(short_explain('longest_run', lr))
        ac = autocorr(bits, maxlag=lag); self.append(short_explain('autocorr', ac))
        peaks, freqs, mag = fft_peaks(bits); self.append(short_explain('fft', peaks))
        cus = cusum(bits); self.append(short_explain('cusum', cus))
        cr = compression_ratio(bits); self.append(short_explain('compress', cr))

        # concluding paragraph — sinteză concluzii în română
        conclusions = []
        # entropy check
        if ent >= 0.995:
            conclusions.append("1) Entropia este foarte înaltă — secvența se comportă extrem de aproape de aleator real.")
        elif ent >= 0.98:
            conclusions.append("1) Entropia e înaltă — mici deviații posibile, dar în general pare aleator.")
        else:
            conclusions.append("1) Entropia e sub așteptări — există indici de structură sau corelare.")
        # monobit + runs + block + serial p-values heuristic
        problematic = []
        if p < 0.01:
            problematic.append("testul monobit (distribuția 0/1)")
        if isinstance(runs, dict) and runs.get('p',1) < 0.01:
            problematic.append("testul runurilor (structură de alternanță)")
        if bf and bf['p'] < 0.01:
            problematic.append("testul block-frequency (distribuții pe blocuri)")
        if ser and ser['p'] < 0.01:
            problematic.append(f"testul serial (m={ser['m']})")
        if peaks and len(peaks)>0:
            problematic.append("spectrul FFT indică potențiale periodicități")

        if not problematic:
            conclusions.append("2) Toate testele statistice folosite în subsetul avansat nu au evidențiat anomalii semnificative (alpha=0.01).")
        else:
            conclusions.append("2) Atenție: următoarele teste au indicat posibile abateri: " + "; ".join(problematic))
            conclusions.append("   Recomandare: analiza detaliată pe secțiuni, verificarea eventualelor canale de inserție și testare contra unei copii independente a fișierului original.")
        conclusions.append(f"3) Compresibilitate (zlib) = {cr:.6f} → {'improbabil de comprimat' if cr>0.95 else 'posibil comprimat'}")
        conclusions.append("4) Aceste rezultate sunt statistice: o singură p-valoare sub prag nu dovedește cu certitudine o inserție, dar merită investigație suplimentară.")
        conclusions.append(f"Timp total analiză: {time.time()-t0:.2f}s")

        # compile report
        rpt = []
        rpt.append("=== RAPORT ANALIZĂ ALEATORIETATE (română) ===\n")
        rpt.append(f"Fișier: {path}\nBiți analizați: {n}\n")
        rpt.append("— Rezultate testelor (sumar):\n")
        rpt.append(f"Entropie Shannon: {ent:.6f} bits/symbol\n")
        rpt.append(f"Monobit: ones={ones}, zeros={zeros}, chi2={chi2:.6f}, p={p:.6e}\n")
        rpt.append(f"Runs test: {runs}\n")
        if bf: rpt.append(f"Block-frequency (bloc {block}): stat={bf['stat']:.6f}, p={bf['p']:.6e}, blocuri={bf['nb_blocks']}\n")
        if ser: rpt.append(f"Serial (m={m}): chi2={ser['chi2']:.6f}, p={ser['p']:.6e}\n")
        rpt.append(f"Approximate entropy (m=2): {apen}\n")
        rpt.append(f"Lempel-Ziv complexity (est): {lz}\n")
        rpt.append(f"Longest run of 1s: {lr}\n")
        rpt.append(f"Autocorrelation (max lag {lag}) — max abs: {np.max(np.abs(ac)) if len(ac)>0 else 0.0}\n")
        rpt.append(f"FFT peaks found: {len(peaks)}\n")
        rpt.append(f"CUSUM: z={cus['z']:.6f}, p~{cus['p']:.6e}\n")
        rpt.append(f"Compression ratio (zlib): {cr:.6f}\n\n")
        rpt.append("— Concluzii (limba română):\n")
        rpt.extend(conclusions)
        rpt_text = "\n".join(rpt)
        self.report_text = rpt_text
        self.append("\n=== CONCLUZII SINTETICE ===")
        for line in conclusions:
            self.append(line)
        self.append("\nRaport complet generat — folosește 'Salvează raport' pentru a-l exporta.\n")
        if problematic:
            self.append("!!! Atenție: teste semnalate. Recomand investigație detaliată.")
        else:
            self.append("Toate testele din acest subset au trecut pragurile alese (alpha=0.01).")
        self.txt.see('1.0')

if __name__ == '__main__':
    try:
        AdvAnalyzer().mainloop()
    except Exception as e:
        print("Eroare:", e)
